diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..76e22be
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "maven" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "daily"
diff --git a/Dockerfile b/Dockerfile
index 411a3dd..1a3f72d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,4 @@
FROM maven:3.8.1-adoptopenjdk-11
MAINTAINER Goudham Suresh
-RUN apt-get update && apt-get install -y \
- gpg
+RUN apt-get update && apt-get install -y gpg
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
index 6974d2f..dbc6054 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -6,12 +6,6 @@ pipeline {
}
environment {
- NEXUS_VERSION = "nexus3"
- NEXUS_PROTOCOL = "https"
- NEXUS_REPOSITORY = "maven-goudham"
- NEXUS_URL = credentials('fe3e0c7e-bcb1-4d55-9591-f55f71f42356')
- NEXUS_CREDENTIAL_ID = 'e5582b32-3507-4e88-ab7c-d16d701c46e9'
-
CODECOV_TOKEN = credentials('44a3c021-5cbb-4a6f-bea2-ae6c51d43038')
GPG_SECRET_KEY = credentials('4dbfd4ed-bba4-44e0-8410-fbce1a9bba73')
@@ -20,6 +14,9 @@ pipeline {
stages {
stage("Import GPG Keys") {
+ when {
+ branch 'release'
+ }
steps {
sh 'gpg --batch --import $GPG_SECRET_KEY'
sh 'gpg --import-ownertrust $GPG_OWNER_TRUST'
@@ -48,7 +45,7 @@ pipeline {
}
}
}
- stage("Deploy") {
+ stage("Deploy To OSSRH") {
when {
branch 'release'
}
diff --git a/README.md b/README.md
index 615cabc..85bbafa 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[license]: https://img.shields.io/github/license/sgoudham/MyWaifuWrapper
[maven-central]: https://img.shields.io/maven-central/v/me.goudham/MyWaifuWrapper
-[build-status]: https://goudham.me/jenkins/job/MyWaifuWrapper/job/release/badge/icon
-[codecov]: https://codecov.io/gh/sgoudham/MyWaifuWrapper/branch/release/graph/badge.svg?token=RxUDnCWnF0
+[build-status]: https://goudham.me/jenkins/job/MyWaifuWrapper/job/main/badge/icon
+[codecov]: https://codecov.io/gh/sgoudham/MyWaifuWrapper/branch/main/graph/badge.svg?token=RxUDnCWnF0
[issues]: https://img.shields.io/github/issues/sgoudham/MyWaifuWrapper?label=issues
[pull-requests]: https://img.shields.io/github/issues-pr/sgoudham/MyWaifuWrapper
[fossa]: https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsgoudham%2FMyWaifuWrapper.svg?type=shield
@@ -15,11 +15,11 @@
![pull-requests]
MyWaifuWrapper
-An Asynchronous Java API Wrapper for MyWaifuList
+An Unofficial Asynchronous Java API Wrapper for MyWaifuList
# Summary
-This is an Asynchronous API Wrapper for [MyWaifuList](https://mywaifulist.moe/dash)
+This is an Unofficial Asynchronous API Wrapper for [MyWaifuList](https://mywaifulist.moe/dash)
# Disclaimer
@@ -28,13 +28,11 @@ the data returned **may not be** fully complete and at its best quality
# Configuration
-## Creating The MyWaifuClient
-
-There are 2 ways to create the [MyWaifuClient](https://github.com/sgoudham/MyWaifuWrapper/blob/main/src/main/java/me/goudham/MyWaifuClient.java)
+There are 2 ways to create and configure the [MyWaifuClient](https://github.com/sgoudham/MyWaifuWrapper/blob/main/src/main/java/me/goudham/MyWaifuClient.java)
+ `createDefault(apiKey)`
+ `build()`
-### createDefault()
+## createDefault()
`createDefault(apiKey)` will provide a default implementation and return a MyWaifuClient ready to be used. Only
the `apiKey` is required to instantiate MyWaifuClient.
@@ -47,10 +45,10 @@ public class Main {
}
```
-### build()
+## build()
`build()` is used to build the object from the ground up, allowing for the fine-tuning of properties within the
-MyWaifuClient. Not all the additional properties need to specified within the builder but the bare minimum would be
+MyWaifuClient. Not all the additional properties need to specified within the builder. However, the bare minimum would be
the `apiKey` within the Builder constructor and then `.build()`
```java
@@ -61,10 +59,11 @@ import java.time.Duration;
public class Main {
private static void main(String[] args) {
- // Bare Minimum (Would recommend using createDefault())
+ // Bare Minimum Config
+ // (Would recommend using createDefault())
MyWaifuClient myWaifuClient = new MyWaifuClient.Builder("apiKey").build();
- // Creating MyWaifuClient through Builder
+ // Creation Through Builder
MyWaifuClient myWaifuClient = new MyWaifuClient.Builder("apiKey")
.withVersion(HttpClient.Version.HTTP_2)
.withConnectTimeout(Duration.ofMinutes(10))
@@ -75,9 +74,51 @@ public class Main {
# Usage
-TODO
+Once **MyWaifuClient** has been configured properly, many methods are available for you to get the data that you wish.
+Each method links up to an endpoint that is listed within the [MyWaifuList API Docs](https://mywaifulist.docs.stoplight.io/api-reference)
+
+With every method executed from the **MyWaifuClient**, a [Response]() object will be returned. The object will house
+the type of entity, response status code, and the response body. This allows for extreme flexibility as the raw response
+and marshalled entity are available to the user.
+
+The documentation for each method provides clear and detailed information on what arguments to pass in, please
+do also refer to that.
+
+Shown below are some examples of how you can retrieve data:
+
+```java
+public class Main {
+ public static void main(String[] args) throws APIMapperException, APIResponseException {
+ // ... myWaifuClient has been instantiated
+
+ // getWaifu(Integer id)
+ Response waifuResponse = myWaifuClient.getWaifu(7);
+ Integer waifuResponseCode = waifuResponse.getStatusCode();
+ String waifuResponseBody = waifuResponse.getBody();
+ Waifu waifu = waifuResponse.getModel();
+
+ // getRandomWaifu()
+ Response randomWaifuResponse = myWaifuClient.getRandomWaifu();
+ Integer randomWaifuResponseCode = randomWaifuResponse.getStatusCode();
+ String randomWaifuResponseBody = randomWaifuResponse.getBody();
+ FilteredWaifu randomWaifu = randomWaifuResponse.getModel();
+
+ // getSeasonalAnime()
+ Response> seasonalAnimeResponse = myWaifuClient.getSeasonalAnime();
+ Integer seasonalAnimeResponseStatusCode = seasonalAnimeResponse.getStatusCode();
+ String seasonalAnimeResponseBody = seasonalAnimeResponse.getBody();
+ List seasonalAnime = seasonalAnimeResponse.getModel();
+
+ // getUserWaifus(Integer id, WaifuListType waifuListType, Integer pageNum)
+ Response> userWaifusLikedResponse = myWaifuClient.getUserWaifus(1, WaifuListType.LIKED, 1);
+ Integer userWaifusLikedResponseStatusCode = userWaifusLikedResponse.getStatusCode();
+ String userWaifusLikedResponseBody = userWaifusLikedResponse.getBody();
+ PaginationData userWaifusLiked = userWaifusLikedResponse.getModel();
+ }
+}
+```
-# Download
+# Installation
Latest Stable Version: ![maven-central]
Be sure to replace the **VERSION** key below with the one of the versions shown above!
diff --git a/pom.xml b/pom.xml
index b61a18a..0b1f086 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,11 +6,11 @@
me.goudham
MyWaifuWrapper
- 0.2.0
+ 1.0.0
jar
MyWaifuWrapper
- An Asynchronous Java API Wrapper for MyWaifuList.
+ An Unofficial Asynchronous Java API Wrapper for MyWaifuList.
https://github.com/sgoudham/MyWaifuWrapper
@@ -62,9 +62,14 @@
org.mockito
mockito-core
- 3.10.0
+ 3.11.2
test
+
+ org.hamcrest
+ hamcrest
+ 2.2
+
org.jetbrains
annotations
@@ -102,7 +107,7 @@
org.apache.maven.plugins
maven-source-plugin
- 3.2.0
+ 3.2.1
attach-sources
@@ -115,7 +120,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.2.0
+ 3.3.0
attach-javadocs
@@ -150,7 +155,7 @@
ossrh
https://s01.oss.sonatype.org/
- true
+ false
diff --git a/src/main/java/me/goudham/APIMapper.java b/src/main/java/me/goudham/APIMapper.java
index e7d460d..1b543c3 100644
--- a/src/main/java/me/goudham/APIMapper.java
+++ b/src/main/java/me/goudham/APIMapper.java
@@ -16,12 +16,28 @@ import java.util.List;
*
*/
class APIMapper {
- private final ObjectMapper objectMapper;
+ private ObjectMapper objectMapper;
APIMapper() {
objectMapper = new ObjectMapper();
}
+ /*\
+ * Convert any object passed and return as a {@link String}
+ *
+ * @param obj {@link Object} to write as {@link String}
+ * @return {@link String}
+ * @throws APIMapperException If {@link ObjectMapper} is not able to serialize object into {@link String}
+ *
+ */
+ String getObjectAsString(Object obj) throws APIMapperException {
+ try {
+ return objectMapper.writeValueAsString(obj);
+ } catch (JsonProcessingException jpe) {
+ throw new APIMapperException(jpe.getMessage(), jpe);
+ }
+ }
+
/**
* Using the given {@code model}, {@link ObjectMapper} deserializes the given Json
* into a Java POJO
@@ -59,6 +75,7 @@ class APIMapper {
* @param The type of model to be returned. E.g {@link Waifu} or {@link Series}
* @return {@link Response}
* @throws APIMapperException If {@link ObjectMapper} is not able to deserialize JSON to Java POJO properly
+ *
*/
Response> deserializeToList(Result result, JavaType model) throws APIMapperException {
Integer statusCode = result.getStatusCode();
@@ -86,6 +103,7 @@ class APIMapper {
* @param The type of model to be returned. E.g {@link Waifu} or {@link Series}
* @return {@link Response}
* @throws APIMapperException If {@link ObjectMapper} is not able to deserialize JSON to Java POJO properly
+ *
*/
Response> deserializeToPaginationData(Result result, JavaType model) throws APIMapperException {
Integer statusCode = result.getStatusCode();
@@ -111,6 +129,7 @@ class APIMapper {
* @return {@link String} — The proper json data to deserialize
* @throws JsonProcessingException If {@link ObjectMapper} is not able to
* read the given {@code jsonBody}
+ *
*/
private String getData(String jsonBody) throws JsonProcessingException {
JsonNode parent = objectMapper.readTree(jsonBody);
@@ -124,6 +143,7 @@ class APIMapper {
*
* @param throwable Type of throwable to throw
* @throws APIMapperException Purpose of the method
+ *
*/
private void throwAPIMapperException(Throwable throwable) throws APIMapperException {
String customExceptionMessage = "If you are seeing this message, " +
@@ -133,4 +153,8 @@ class APIMapper {
throw new APIMapperException(exceptionMessage, throwable);
}
+
+ void setObjectMapper(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ }
}
diff --git a/src/main/java/me/goudham/APIUtils.java b/src/main/java/me/goudham/APIUtils.java
index f0b7566..38d04ed 100644
--- a/src/main/java/me/goudham/APIUtils.java
+++ b/src/main/java/me/goudham/APIUtils.java
@@ -28,6 +28,7 @@ class APIUtils {
* @param model The actual class of the given model. E.g {@link Waifu#getClass()}
* @param The type of model to be returned. E.g {@link Waifu} or {@link Series}
* @return {@link JavaType} of {@link PaginationData}
+ *
*/
static JavaType paginationData(Class model) {
return TypeFactory.defaultInstance().constructParametricType(PaginationData.class, model);
diff --git a/src/main/java/me/goudham/APIWrapper.java b/src/main/java/me/goudham/APIWrapper.java
index a7fca8a..a786ec1 100644
--- a/src/main/java/me/goudham/APIWrapper.java
+++ b/src/main/java/me/goudham/APIWrapper.java
@@ -20,6 +20,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -31,302 +32,362 @@ import static me.goudham.APIUtils.paginationData;
/**
* Returns API information to {@link MyWaifuClient}
- *
*/
-public class APIWrapper {
- private final String version = "1.0";
- private static final String host = "https://mywaifulist.moe/api/v1/";
- private final String apiKey;
-
- private final APIMapper apiMapper;
- private final HttpClient httpClient;
- private final Executor executor = Executors.newFixedThreadPool(10);
-
- /**
- * Instantiates an instance of {@link APIWrapper} to retrieve API Information.
- * An instance of {@link APIMapper} is created to be able to {@code deserialize} JSON to
- * Java objects
- *
- * @param apiKey API Key to authorise API request
- * @param httpClient The underlying {@link HttpClient} to use for HttpRequests
- *
- */
- APIWrapper(String apiKey, HttpClient httpClient) {
- this.apiKey = apiKey;
- this.httpClient = httpClient;
- apiMapper = new APIMapper();
- }
-
- /**
- * Handles sending a request to the API asynchronously using {@link HttpRequest}
- * and the underlying {@link HttpClient}
- *
- * @param param The end of the endpoint appended onto the host
- * @return {@link Result}
- * @throws APIResponseException If the {@link CompletableFuture Response}
- * cannot be decoded or the thread was interrupted while waiting to receive the data
- *
- */
- private Result sendRequest(String param) throws APIResponseException {
- CompletableFuture futureResult = CompletableFuture.supplyAsync(() -> {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(host + param))
- .version(httpClient.version())
- .timeout(Duration.ofSeconds(20))
- .headers("Content-Type", "application/json", "apikey", apiKey)
- .GET()
- .build();
-
- try {
- return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
- } catch (IOException | InterruptedException exp) {
- exp.printStackTrace();
- }
- return null;
- }, executor).thenApply(httpResponse -> new Result(httpResponse.statusCode(), httpResponse.body()));
-
- try {
- return futureResult.get();
- } catch (InterruptedException | ExecutionException exp) {
- throw new APIResponseException(exp.getMessage(), exp);
- }
- }
-
- /**
- * Retrieve detailed information about the {@link Waifu} by sending request to API
- *
- * @param waifuId The id of the {@link Waifu}
- * @return {@link Response} of {@link Waifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response getWaifu(String waifuId) throws APIResponseException, APIMapperException {
- Result waifuResult = sendRequest("waifu/" + waifuId);
- return apiMapper.deserialize(waifuResult, Waifu.class);
- }
-
- /**
- * Retrieve paginated images from the gallery, in sets of 10, by sending request to API
- *
- * @param waifuId The id of the {@link Waifu}
- * @param pageNum The page number of the gallery
- * @return {@link Response} of {@link WaifuImage}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getWaifuImages(String waifuId, String pageNum) throws APIResponseException, APIMapperException {
- Result waifuImagesResult = sendRequest("waifu/" + waifuId + "/images?page=" + pageNum);
- return apiMapper.deserializeToPaginationData(waifuImagesResult, paginationData(WaifuImage.class));
- }
-
- /**
- * Retrieve an array of {@link FilteredWaifu}'s, sorted alphabetically, by sending request to API
- *
- * @param pageNum The page number of the gallery
- * @return {@link Response} of {@link PaginationData} with {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getWaifusByPage(String pageNum) throws APIResponseException, APIMapperException {
- Result waifusByPageResult = sendRequest("waifu?page=" + pageNum);
- return apiMapper.deserializeToPaginationData(waifusByPageResult, paginationData(FilteredWaifu.class));
- }
-
- /**
- * Retrieve the Waifu of the Day by sending request to API
- *
- * @return {@link Response} of {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response getDailyWaifu() throws APIResponseException, APIMapperException {
- Result dailyWaifuResult = sendRequest("meta/daily");
- return apiMapper.deserialize(dailyWaifuResult, FilteredWaifu.class);
- }
-
- /**
- * Retrieve a Random Waifu from the Website by sending request to API
- *
- * @return {@link Response} of {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response getRandomWaifu() throws APIResponseException, APIMapperException {
- Result randomWaifuResult = sendRequest("meta/random");
- return apiMapper.deserialize(randomWaifuResult, FilteredWaifu.class);
- }
-
- /**
- * Retrieve a List of Currently Airing Anim by sending request to API
- *
- * @return {@link Response} of {@link List} with {@link FilteredSeries}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getSeasonalAnime() throws APIResponseException, APIMapperException {
- Result seasonalAnimeResult = sendRequest("airing");
- return apiMapper.deserializeToList(seasonalAnimeResult, listOf(FilteredSeries.class));
- }
-
- /**
- * Retrieve the Best Waifus of the Current Season by sending request to API
- *
- * @return {@link Response} of {@link List} with {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getBestWaifus() throws APIResponseException, APIMapperException {
- Result waifuResults = sendRequest("airing/best");
- return apiMapper.deserializeToList(waifuResults, listOf(FilteredWaifu.class));
- }
-
- /**
- * Retrieve a List of Popular Waifus (Raw Count of Total Votes) of the Current Season
- * by sending request to API
- *
- * @return {@link Response} of {@link List} with {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getPopularWaifus() throws APIResponseException, APIMapperException {
- Result waifuResults = sendRequest("airing/popular");
- return apiMapper.deserializeToList(waifuResults, listOf(FilteredWaifu.class));
- }
-
- /**
- * Retrieve the Most Disliked Waifus of the Current Season by sending request to API
- *
- * @return {@link Response} of {@link List} with {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getTrashWaifus() throws APIResponseException, APIMapperException {
- Result waifuResults = sendRequest("airing/trash");
- return apiMapper.deserializeToList(waifuResults, listOf(FilteredWaifu.class));
- }
-
- /**
- * Retrieve detailed information about a given {@link Series} by sending request to API
- *
- * @param seriesId The id of the {@link Series}
- * @return {@link Response} of {@link Series}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response getSeries(String seriesId) throws APIResponseException, APIMapperException {
- Result seriesResult = sendRequest("series/" + seriesId);
- return apiMapper.deserialize(seriesResult, Series.class);
- }
-
- /**
- * Retrieve paginated information about a Series by sending request to API
- *
- * @param pageNum The page number of the gallery
- * @return {@link Response} of {@link PaginationData} with {@link FilteredSeries}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getSeriesByPage(String pageNum) throws APIResponseException, APIMapperException {
- Result seriesPageResult = sendRequest("series?page=" + pageNum);
- return apiMapper.deserializeToPaginationData(seriesPageResult, paginationData(FilteredSeries.class));
- }
-
- /**
- * Retrieve the List of Anime that Aired in a given Season and Year by sending request to API
- *
- * @param season The specified season from {@link Season}
- * @param year The specified year
- * @return {@link Response} of {@link List} with {@link FilteredSeries}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getAllSeries(Season season, Integer year) throws APIResponseException, APIMapperException {
- Result allSeriesResult = sendRequest("airing/" + season.getSeason() + "/" + year);
- return apiMapper.deserializeToList(allSeriesResult, listOf(FilteredSeries.class));
- }
-
- /**
- * Retrieve a set of Waifus for a given {@link Series} by sending request to API
- *
- * @param seriesId The id of the {@link Series}
- * @return {@link Response} of {@link List} with {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getSeriesWaifus(String seriesId) throws APIResponseException, APIMapperException {
- Result allWaifusFromSeriesResults = sendRequest("series/" + seriesId + "/waifus");
- return apiMapper.deserializeToList(allWaifusFromSeriesResults, listOf(FilteredWaifu.class));
- }
-
- /**
- * Retrieve information about the {@link User} by sending request to API
- *
- * @param userId The id of the {@link User}
- * @return {@link Response} of {@link User}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response getUserProfile(String userId) throws APIResponseException, APIMapperException {
- Result userProfileResult = sendRequest("user/" + userId);
- return apiMapper.deserialize(userProfileResult, User.class);
- }
-
- /**
- * Retrieve the Waifus Created, Liked, or Trashed for the given {@link User} id by sending request to API
- *
- * @param userId The id of the {@link User}
- * @param listType The specified action E.g {@link WaifuListType#LIKED}
- * @param pageNum The page number of the gallery
- * @return {@link Response} of {@link PaginationData} with {@link FilteredWaifu}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getUserWaifus(String userId, String listType, String pageNum) throws APIResponseException, APIMapperException {
- Result userWaifusResult = sendRequest("user/" + userId + "/" + listType + "?page=" + pageNum);
- return apiMapper.deserializeToPaginationData(userWaifusResult, paginationData(FilteredWaifu.class));
- }
-
- /**
- * Retrieve a List of all {@link UserList}'s shown by sending request to API
- *
- * @param userId The id of the {@link User}
- * @return {@link Response} of {@link List} with {@link UserList}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response> getUserLists(String userId) throws APIResponseException, APIMapperException {
- Result userProfileResult = sendRequest("user/" + userId + "/lists");
- return apiMapper.deserializeToList(userProfileResult, listOf(UserList.class));
- }
-
- /**
- * Retrieve the Specific {@link UserList}, with {@link Waifu}'s by sending request to API
- *
- * @param userId The id of the {@link User}
- * @param listId The id of the {@link UserList}
- * @return {@link Response} of {@link UserList}
- * @throws APIResponseException If {@link APIWrapper} could not return information properly
- * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
- *
- */
- Response getUserList(String userId, String listId) throws APIResponseException, APIMapperException {
- Result userProfileResult = sendRequest("user/" + userId + "/lists/" + listId);
- return apiMapper.deserialize(userProfileResult, UserList.class);
- }
+class APIWrapper {
+ private final String version = "1.0";
+ private static final String host = "https://mywaifulist.moe/api/v1/";
+ private String apiKey;
+
+ private APIMapper apiMapper;
+ private final HttpClient httpClient;
+ private final Executor executor = Executors.newFixedThreadPool(10);
+
+ /**
+ * Instantiates an instance of {@link APIWrapper} to retrieve API Information.
+ * An instance of {@link APIMapper} is created to be able to {@code deserialize} JSON to
+ * Java objects
+ *
+ * @param apiKey API Key to authorise API request
+ * @param httpClient The underlying {@link HttpClient} to use for HttpRequests
+ */
+ APIWrapper(String apiKey, HttpClient httpClient) {
+ this.apiKey = apiKey;
+ this.httpClient = httpClient;
+ apiMapper = new APIMapper();
+ }
+
+ /**
+ * Create base {@link HttpRequest.Builder} with custom url, default headers and timeout
+ *
+ * @param param The end of the endpoint appended onto the host
+ * @return {@link HttpRequest.Builder}
+ */
+ private HttpRequest.Builder getBaseRequest(String param) {
+ return HttpRequest.newBuilder()
+ .uri(URI.create(host + param))
+ .timeout(Duration.ofSeconds(20))
+ .headers("Content-Type", "application/json", "apikey", apiKey);
+ }
+
+ /**
+ * Separate method for sending GET requests
+ *
+ * @param param The end of the endpoint appended onto the host
+ * @return {@link Result}
+ * @throws APIResponseException If {@link #sendRequest(HttpRequest)} cannot retrieve the proper data from the API
+ */
+ Result sendGetRequest(String param) throws APIResponseException {
+ HttpRequest request = getBaseRequest(param).GET().build();
+ return sendRequest(request);
+ }
+
+ /**
+ * Separate method for sending POST requests
+ *
+ * @param param The end of the endpoint appended onto the host
+ * @param headers Headers as Key/Value pairs for POST requests
+ * @return {@link Result}
+ * @throws APIResponseException If {@link #sendRequest(HttpRequest)} cannot retrieve the proper data from the API
+ * @throws APIMapperException If {@link APIMapper#getObjectAsString(Object)} cannot properly serialize object
+ */
+ private Result sendPostRequest(String param, Map headers) throws APIResponseException, APIMapperException {
+ HttpRequest request = getBaseRequest(param)
+ .POST(HttpRequest.BodyPublishers.ofString(apiMapper.getObjectAsString(headers)))
+ .build();
+ return sendRequest(request);
+ }
+
+ /**
+ * Handles sending a request to the API asynchronously using {@link HttpRequest}
+ * and the underlying {@link HttpClient}
+ *
+ * @param httpRequest The {@link HttpRequest} to be sent by the {@link HttpClient}
+ * @return {@link Result}
+ * @throws APIResponseException If the {@link CompletableFuture Response}
+ * cannot be decoded or the thread was interrupted while waiting to receive the data
+ */
+ private Result sendRequest(HttpRequest httpRequest) throws APIResponseException {
+ CompletableFuture futureResult = CompletableFuture.supplyAsync(() -> {
+ try {
+ return httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+ } catch (IOException | InterruptedException exp) {
+ exp.printStackTrace();
+ }
+ return null;
+ }, executor).thenApply(httpResponse -> new Result(httpResponse.statusCode(), httpResponse.body()));
+
+ try {
+ return futureResult.get();
+ } catch (InterruptedException | ExecutionException exp) {
+ throw new APIResponseException(exp.getMessage(), exp);
+ }
+ }
+
+ /**
+ * Retrieve detailed information about the {@link Waifu} by sending request to API
+ *
+ * @param waifuId The id of the {@link Waifu}
+ * @return {@link Response} of {@link Waifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response getWaifu(String waifuId) throws APIResponseException, APIMapperException {
+ Result waifuResult = sendGetRequest("waifu/" + waifuId);
+ return apiMapper.deserialize(waifuResult, Waifu.class);
+ }
+
+ /**
+ * Retrieve paginated images from the gallery, in sets of 10, by sending request to API
+ *
+ * @param waifuId The id of the {@link Waifu}
+ * @param pageNum The page number of the gallery
+ * @return {@link Response} of {@link WaifuImage}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getWaifuImages(String waifuId, String pageNum) throws APIResponseException, APIMapperException {
+ Result waifuImagesResult = sendGetRequest("waifu/" + waifuId + "/images?page=" + pageNum);
+ return apiMapper.deserializeToPaginationData(waifuImagesResult, paginationData(WaifuImage.class));
+ }
+
+ /**
+ * Retrieve an array of {@link FilteredWaifu}'s, sorted alphabetically, by sending request to API
+ *
+ * @param pageNum The page number of the gallery
+ * @return {@link Response} of {@link PaginationData} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getWaifusByPage(String pageNum) throws APIResponseException, APIMapperException {
+ Result waifusByPageResult = sendGetRequest("waifu?page=" + pageNum);
+ return apiMapper.deserializeToPaginationData(waifusByPageResult, paginationData(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve the Waifu of the Day by sending request to API
+ *
+ * @return {@link Response} of {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response getDailyWaifu() throws APIResponseException, APIMapperException {
+ Result dailyWaifuResult = sendGetRequest("meta/daily");
+ return apiMapper.deserialize(dailyWaifuResult, FilteredWaifu.class);
+ }
+
+ /**
+ * Retrieve a Random Waifu from the Website by sending request to API
+ *
+ * @return {@link Response} of {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response getRandomWaifu() throws APIResponseException, APIMapperException {
+ Result randomWaifuResult = sendGetRequest("meta/random");
+ return apiMapper.deserialize(randomWaifuResult, FilteredWaifu.class);
+ }
+
+ /**
+ * Retrieve a List of Currently Airing Anim by sending request to API
+ *
+ * @return {@link Response} of {@link List} with {@link FilteredSeries}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getSeasonalAnime() throws APIResponseException, APIMapperException {
+ Result seasonalAnimeResult = sendGetRequest("airing");
+ return apiMapper.deserializeToList(seasonalAnimeResult, listOf(FilteredSeries.class));
+ }
+
+ /**
+ * Retrieve the Best Waifus of the Current Season by sending request to API
+ *
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getBestWaifus() throws APIResponseException, APIMapperException {
+ Result waifuResults = sendGetRequest("airing/best");
+ return apiMapper.deserializeToList(waifuResults, listOf(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve a List of Popular Waifus (Raw Count of Total Votes) of the Current Season
+ * by sending request to API
+ *
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getPopularWaifus() throws APIResponseException, APIMapperException {
+ Result waifuResults = sendGetRequest("airing/popular");
+ return apiMapper.deserializeToList(waifuResults, listOf(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve the Most Disliked Waifus of the Current Season by sending request to API
+ *
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getTrashWaifus() throws APIResponseException, APIMapperException {
+ Result waifuResults = sendGetRequest("airing/trash");
+ return apiMapper.deserializeToList(waifuResults, listOf(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve detailed information about a given {@link Series} by sending request to API
+ *
+ * @param seriesId The id of the {@link Series}
+ * @return {@link Response} of {@link Series}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response getSeries(String seriesId) throws APIResponseException, APIMapperException {
+ Result seriesResult = sendGetRequest("series/" + seriesId);
+ return apiMapper.deserialize(seriesResult, Series.class);
+ }
+
+ /**
+ * Retrieve paginated information about a Series by sending request to API
+ *
+ * @param pageNum The page number of the gallery
+ * @return {@link Response} of {@link PaginationData} with {@link FilteredSeries}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getSeriesByPage(String pageNum) throws APIResponseException, APIMapperException {
+ Result seriesPageResult = sendGetRequest("series?page=" + pageNum);
+ return apiMapper.deserializeToPaginationData(seriesPageResult, paginationData(FilteredSeries.class));
+ }
+
+ /**
+ * Retrieve the List of Anime that Aired in a given Season and Year by sending request to API
+ *
+ * @param season The specified season from {@link Season}
+ * @param year The specified year
+ * @return {@link Response} of {@link List} with {@link FilteredSeries}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getAllSeries(Season season, Integer year) throws APIResponseException, APIMapperException {
+ Result allSeriesResult = sendGetRequest("airing/" + season.getSeason() + "/" + year);
+ return apiMapper.deserializeToList(allSeriesResult, listOf(FilteredSeries.class));
+ }
+
+ /**
+ * Retrieve a set of Waifus for a given {@link Series} by sending request to API
+ *
+ * @param seriesId The id of the {@link Series}
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getSeriesWaifus(String seriesId) throws APIResponseException, APIMapperException {
+ Result allWaifusFromSeriesResults = sendGetRequest("series/" + seriesId + "/waifus");
+ return apiMapper.deserializeToList(allWaifusFromSeriesResults, listOf(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve information about the {@link User} by sending request to API
+ *
+ * @param userId The id of the {@link User}
+ * @return {@link Response} of {@link User}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response getUserProfile(String userId) throws APIResponseException, APIMapperException {
+ Result userProfileResult = sendGetRequest("user/" + userId);
+ return apiMapper.deserialize(userProfileResult, User.class);
+ }
+
+ /**
+ * Retrieve the Waifus Created, Liked, or Trashed for the given {@link User} id by sending request to API
+ *
+ * @param userId The id of the {@link User}
+ * @param listType The specified action E.g {@link WaifuListType#LIKED}
+ * @param pageNum The page number of the gallery
+ * @return {@link Response} of {@link PaginationData} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getUserWaifus(String userId, String listType, String pageNum) throws APIResponseException, APIMapperException {
+ Result userWaifusResult = sendGetRequest("user/" + userId + "/" + listType + "?page=" + pageNum);
+ return apiMapper.deserializeToPaginationData(userWaifusResult, paginationData(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve a List of all {@link UserList}'s shown by sending request to API
+ *
+ * @param userId The id of the {@link User}
+ * @return {@link Response} of {@link List} with {@link UserList}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> getUserLists(String userId) throws APIResponseException, APIMapperException {
+ Result userProfileResult = sendGetRequest("user/" + userId + "/lists");
+ return apiMapper.deserializeToList(userProfileResult, listOf(UserList.class));
+ }
+
+ /**
+ * Retrieve the Specific {@link UserList}, with {@link Waifu}'s by sending request to API
+ *
+ * @param userId The id of the {@link User}
+ * @param listId The id of the {@link UserList}
+ * @return {@link Response} of {@link UserList}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response getUserList(String userId, String listId) throws APIResponseException, APIMapperException {
+ Result userProfileResult = sendGetRequest("user/" + userId + "/lists/" + listId);
+ return apiMapper.deserialize(userProfileResult, UserList.class);
+ }
+
+ /**
+ * Retrieve a {@link List} of {@link FilteredWaifu} with a search term by sending a POST request to the API
+ *
+ * @param searchString {@link String} that should be searched for
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> betaSearch(String searchString) throws APIMapperException, APIResponseException {
+ Result betaSearchResult = sendPostRequest("search/beta", Map.of("term", searchString));
+ return apiMapper.deserializeToList(betaSearchResult, listOf(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve a List of {@link FilteredWaifu}'s given a query, by sending POST request to API
+ *
+ * @param waifuName The name of the Waifu
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> searchWaifus(String waifuName) throws APIMapperException, APIResponseException {
+ Result searchWaifusResult = sendPostRequest("search/waifus", Map.of("term", waifuName));
+ return apiMapper.deserializeToList(searchWaifusResult, listOf(FilteredWaifu.class));
+ }
+
+ /**
+ * Retrieve a List of {@link FilteredSeries}'s given a query, by sending POST request to API
+ *
+ * @param seriesName The name of the Series
+ * @return {@link Response} of {@link List} with {@link FilteredSeries}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ Response> searchSeries(String seriesName) throws APIMapperException, APIResponseException {
+ Result searchSeriesResult = sendPostRequest("search/series", Map.of("term", seriesName));
+ return apiMapper.deserializeToList(searchSeriesResult, listOf(FilteredSeries.class));
+ }
+
+ void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ void setApiMapper(APIMapper apiMapper) {
+ this.apiMapper = apiMapper;
+ }
}
diff --git a/src/main/java/me/goudham/MyWaifuClient.java b/src/main/java/me/goudham/MyWaifuClient.java
index 95b811d..09df518 100644
--- a/src/main/java/me/goudham/MyWaifuClient.java
+++ b/src/main/java/me/goudham/MyWaifuClient.java
@@ -32,7 +32,7 @@ import java.util.concurrent.Executor;
*
*/
public class MyWaifuClient {
- private final APIWrapper APIWrapper;
+ private APIWrapper APIWrapper;
/**
* Creates an instance of {@link MyWaifuClient}
@@ -42,7 +42,7 @@ public class MyWaifuClient {
* @param httpClient The underlying {@link HttpClient} to use for HttpRequests
*
*/
- MyWaifuClient(@NotNull String apiKey, @NotNull HttpClient httpClient) {
+ private MyWaifuClient(@NotNull String apiKey, @NotNull HttpClient httpClient) {
APIWrapper = new APIWrapper(apiKey, httpClient);
}
@@ -328,6 +328,49 @@ public class MyWaifuClient {
return APIWrapper.getUserList(String.valueOf(userId), String.valueOf(listId));
}
+ /**
+ * Allows searching for a {@link Waifu}
+ * This search is more aggressive when it comes to name matching, resulting in better accuracy in most cases
+ *
+ * @param searchString {@link String} that should be searched for
+ * @return {@link Response} of {@link List} with {@link FilteredWaifu}
+ * @throws APIResponseException If {@link APIWrapper} could not return information properly
+ * @throws APIMapperException If {@link APIMapper} could not correctly {@code deserialize} model
+ */
+ public Response> betaSearch(@NotNull String searchString) throws APIResponseException, APIMapperException {
+ return APIWrapper.betaSearch(searchString);
+ }
+
+ /**
+ * Searches only Waifu's using a given query. The higher the relevance, the better the match
+ *
+ * @param name The name of the Waifu
+ * @return {@link Response} of {@link FilteredWaifu}
+ * @throws APIMapperException If {@link APIWrapper} could not return information properly
+ * @throws APIResponseException If {@link APIMapper} could not correctly {@code deserialize} model
+ *
+ */
+ public Response> searchWaifus(@NotNull String name) throws APIMapperException, APIResponseException {
+ return APIWrapper.searchWaifus(name);
+ }
+
+ /**
+ * Searches only Series' using a given query. The higher the relevance, the better the match
+ *
+ * @param name The name of the Series
+ * @return {@link Response} of {@link FilteredSeries}
+ * @throws APIMapperException If {@link APIWrapper} could not return information properly
+ * @throws APIResponseException If {@link APIMapper} could not correctly {@code deserialize} model
+ *
+ */
+ public Response> searchSeries(@NotNull String name) throws APIMapperException, APIResponseException {
+ return APIWrapper.searchSeries(name);
+ }
+
+ void setAPIWrapper(me.goudham.APIWrapper APIWrapper) {
+ this.APIWrapper = APIWrapper;
+ }
+
/**
* Builder for {@link MyWaifuClient}
*
diff --git a/src/main/java/me/goudham/domain/waifu/Waifu.java b/src/main/java/me/goudham/domain/waifu/Waifu.java
index 12711a4..30ca185 100644
--- a/src/main/java/me/goudham/domain/waifu/Waifu.java
+++ b/src/main/java/me/goudham/domain/waifu/Waifu.java
@@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.*;
import me.goudham.domain.series.Series;
import me.goudham.domain.user.Creator;
-import javax.annotation.processing.Generated;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -659,7 +658,6 @@ public class Waifu {
* Common in Japanese culture as a potential representation of personality types
*
*/
- @Generated("jsonschema2pojo")
public enum BloodType {
A("A"),
@@ -668,7 +666,7 @@ public class Waifu {
AB("AB");
private final String value;
- private final static Map CONSTANTS = new HashMap();
+ private final static Map CONSTANTS = new HashMap<>();
static {
for (Waifu.BloodType bloodType: values()) {
diff --git a/src/main/java/me/goudham/exception/APIMapperException.java b/src/main/java/me/goudham/exception/APIMapperException.java
index 9e5dd04..e44e225 100644
--- a/src/main/java/me/goudham/exception/APIMapperException.java
+++ b/src/main/java/me/goudham/exception/APIMapperException.java
@@ -1,10 +1,9 @@
package me.goudham.exception;
-import me.goudham.APIWrapper;
import me.goudham.Response;
/**
- * Thrown when {@link APIWrapper} fails to deserialize json into Java POJO's ({@link Response#getModel()})
+ * Thrown when {@code APIWrapper} fails to deserialize json into Java POJO's ({@link Response#getModel()})
*
*/
public class APIMapperException extends Throwable {
diff --git a/src/main/java/me/goudham/exception/APIResponseException.java b/src/main/java/me/goudham/exception/APIResponseException.java
index f1a3439..018e513 100644
--- a/src/main/java/me/goudham/exception/APIResponseException.java
+++ b/src/main/java/me/goudham/exception/APIResponseException.java
@@ -1,9 +1,7 @@
package me.goudham.exception;
-import me.goudham.APIWrapper;
-
/**
- * Thrown when {@link APIWrapper} fails to return API information
+ * Thrown when {@code APIWrapper} fails to return API information
*
*/
public class APIResponseException extends Throwable {
diff --git a/src/test/java/me/goudham/APIWrapperTest.java b/src/test/java/me/goudham/APIWrapperTest.java
new file mode 100644
index 0000000..fc35e55
--- /dev/null
+++ b/src/test/java/me/goudham/APIWrapperTest.java
@@ -0,0 +1,123 @@
+package me.goudham;
+
+import me.goudham.exception.APIResponseException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import javax.net.ssl.SSLSession;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Optional;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.*;
+
+class APIWrapperTest {
+
+ @Mock
+ private HttpClient httpClient;
+
+ private APIWrapper sut;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ sut = new APIWrapper("ValidAPIKey", httpClient);
+ }
+
+ @Test
+ void successfullyReturn400WhenApiTokenInvalid() throws APIResponseException, IOException, InterruptedException {
+ HttpRequest expectedHttpRequest = buildHttpRequest("InvalidAPIKey");
+ int expectedStatusCode = 400;
+ String expectedBody = "{\"message\":\"Access denied - please check your token\",\"code\":400}";
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+
+ sut.setApiKey("InvalidAPIKey");
+ Result actualResult = sut.sendGetRequest("waifu/1");
+
+ assertThat(actualResult.getStatusCode(), is(expectedStatusCode));
+ assertThat(actualResult.getBody(), is(expectedBody));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ @Test
+ void successfullyReturn200WhenApiTokenValid() throws APIResponseException, IOException, InterruptedException {
+ HttpRequest expectedHttpRequest = buildHttpRequest("ValidAPIKey");
+ int expectedStatusCode = 200;
+ String expectedBody = "{\"message\":\"Token Valid\",\"code\":200}";
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+
+ Result actualResult = sut.sendGetRequest("waifu/1");
+
+ assertThat(actualResult.getStatusCode(), is(expectedStatusCode));
+ assertThat(actualResult.getBody(), is(expectedBody));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ private HttpRequest buildHttpRequest(String apiKey) {
+ return HttpRequest.newBuilder()
+ .uri(URI.create("https://mywaifulist.moe/api/v1/waifu/1"))
+ .timeout(Duration.ofSeconds(20))
+ .headers("Content-Type", "application/json", "apikey", apiKey)
+ .GET()
+ .build();
+ }
+
+ private HttpResponse buildHttpResponse(int statusCode, String body) {
+ return new HttpResponse<>() {
+ @Override
+ public int statusCode() {
+ return statusCode;
+ }
+
+ @Override
+ public HttpRequest request() {
+ return null;
+ }
+
+ @Override
+ public Optional> previousResponse() {
+ return Optional.empty();
+ }
+
+ @Override
+ public HttpHeaders headers() {
+ return null;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+
+ @Override
+ public Optional sslSession() {
+ return Optional.empty();
+ }
+
+ @Override
+ public URI uri() {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Version version() {
+ return null;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/me/goudham/MyWaifuClientTest.java b/src/test/java/me/goudham/MyWaifuClientTest.java
new file mode 100644
index 0000000..ccddc89
--- /dev/null
+++ b/src/test/java/me/goudham/MyWaifuClientTest.java
@@ -0,0 +1,244 @@
+package me.goudham;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import me.goudham.domain.pagination.PaginationData;
+import me.goudham.domain.waifu.FilteredWaifu;
+import me.goudham.domain.waifu.Waifu;
+import me.goudham.domain.waifu.WaifuImage;
+import me.goudham.exception.APIMapperException;
+import me.goudham.exception.APIResponseException;
+import me.goudham.util.TestEntity;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import javax.net.ssl.SSLSession;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static me.goudham.APIUtils.listOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+class MyWaifuClientTest {
+
+ @Mock
+ private HttpClient httpClient;
+
+ @Spy
+ private ObjectMapper objectMapper;
+
+ private final String apiKey = "ValidAPIKey";
+
+ private MyWaifuClient sut;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+
+ sut = MyWaifuClient.createDefault(apiKey);
+ APIWrapper apiWrapper = new APIWrapper(apiKey, httpClient);
+ APIMapper apiMapper = new APIMapper();
+
+ apiMapper.setObjectMapper(objectMapper);
+ apiWrapper.setApiMapper(apiMapper);
+ sut.setAPIWrapper(apiWrapper);
+ }
+
+ @Test
+ void successfullyGetWaifu() throws IOException, InterruptedException, APIMapperException, APIResponseException {
+ HttpRequest expectedHttpRequest = buildHttpGetRequest(apiKey, "waifu/1");
+ int expectedStatusCode = 200;
+ String expectedBody = getJsonAsString("getWaifu.json");
+ Waifu expectedWaifu = TestEntity.getExpectedWaifu();
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+
+ Response actualWaifuResponse = sut.getWaifu(1);
+
+ assertThat(actualWaifuResponse.getStatusCode(), is(expectedStatusCode));
+ assertThat(actualWaifuResponse.getBody(), is(expectedBody));
+ assertThat(actualWaifuResponse.getModel(), is(expectedWaifu));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ @Test
+ void successfullyGetBestWaifus() throws IOException, InterruptedException, APIMapperException, APIResponseException {
+ HttpRequest expectedHttpRequest = buildHttpGetRequest(apiKey, "airing/best");
+ int expectedStatusCode = 200;
+ String expectedBody = getJsonAsString("getBestWaifus.json");
+ List expectedBestWaifus = TestEntity.getBestWaifus();
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+
+ Response> actualBestWaifusResponse = sut.getBestWaifus();
+
+ assertThat(actualBestWaifusResponse.getStatusCode(), is(expectedStatusCode));
+ assertThat(actualBestWaifusResponse.getBody(), is(expectedBody));
+ assertThat(actualBestWaifusResponse.getModel(), is(expectedBestWaifus));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ @Test
+ void successfullyGetWaifuImages() throws IOException, InterruptedException, APIMapperException, APIResponseException {
+ HttpRequest expectedHttpRequest = buildHttpGetRequest(apiKey, "waifu/1/images?page=1");
+ int expectedStatusCode = 200;
+ String expectedBody = getJsonAsString("getWaifuImages.json");
+ PaginationData expectedWaifuImages = TestEntity.getWaifuImages();
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+
+ Response> actualWaifuImagesResponse = sut.getWaifuImages(1, 1);
+
+ assertThat(actualWaifuImagesResponse.getStatusCode(), is(expectedStatusCode));
+ assertThat(actualWaifuImagesResponse.getBody(), is(expectedBody));
+ assertThat(actualWaifuImagesResponse.getModel(), is(expectedWaifuImages));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ @Test
+ void successfullyPostBetaSearch() throws IOException, InterruptedException, APIMapperException, APIResponseException {
+ HttpRequest expectedHttpRequest = buildHttpPostRequest(apiKey, "search/beta");
+ int expectedStatusCode = 200;
+ String expectedBody = getJsonAsString("betaSearch.json");
+ List expectedBetaSearch = TestEntity.getBetaSearch();
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+
+ Response> actualBetaSearchResponse = sut.betaSearch("Yumeko");
+
+ assertThat(actualBetaSearchResponse.getStatusCode(), is(expectedStatusCode));
+ assertThat(actualBetaSearchResponse.getBody(), is(expectedBody));
+ assertThat(actualBetaSearchResponse.getModel(), is(expectedBetaSearch));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ @Test
+ void failToGetBestWaifusWhereDeserializationGoesWrong() throws IOException, InterruptedException {
+ HttpRequest expectedHttpRequest = buildHttpGetRequest(apiKey, "airing/best");
+ int expectedStatusCode = 200;
+ String expectedBody = getJsonAsString("getBestWaifus.json");
+ String expectedData = getData(expectedBody);
+ HttpResponse expectedHttpResponse = buildHttpResponse(expectedStatusCode, expectedBody);
+
+ JavaType expectedModel = listOf(FilteredWaifu.class);
+ String customExceptionMessage = "If you are seeing this message, " +
+ "this is more than likely a fault in my logic. " +
+ "Please raise an issue including the printed stacktrace :D";
+ String exceptionMessage = "Uh Oh Somebody Did a No No!";
+ String expectedExceptionMessage = "\n\n" + customExceptionMessage + "\n\n" + exceptionMessage;
+
+ doReturn(expectedHttpResponse).when(httpClient).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ doThrow(new JsonProcessingException(exceptionMessage) {}).when(objectMapper).readValue(expectedData, expectedModel);
+
+ Throwable actualException = assertThrows(APIMapperException.class, () -> sut.getBestWaifus());
+
+ assertThat(actualException.getMessage(), is(expectedExceptionMessage));
+ verify(httpClient, times(1)).send(expectedHttpRequest, HttpResponse.BodyHandlers.ofString());
+ verifyNoMoreInteractions(httpClient);
+ }
+
+ private String getData(String jsonBody) throws JsonProcessingException {
+ JsonNode parent = objectMapper.readTree(jsonBody);
+ return parent.get("data").toString();
+ }
+
+ private HttpRequest buildHttpPostRequest(String apiKey, String param) {
+ return HttpRequest.newBuilder()
+ .uri(URI.create("https://mywaifulist.moe/api/v1/" + param))
+ .timeout(Duration.ofSeconds(20))
+ .headers("Content-Type", "application/json", "apikey", apiKey)
+ .POST(HttpRequest.BodyPublishers.ofString(""))
+ .build();
+ }
+
+
+ private String getJsonAsString(String filename) {
+ InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(filename);
+ return new BufferedReader(new InputStreamReader(Objects.requireNonNull(resourceAsStream), StandardCharsets.UTF_8))
+ .lines()
+ .collect(Collectors.joining("\n"));
+ }
+
+ private HttpRequest buildHttpGetRequest(String apiKey, String param) {
+ return HttpRequest.newBuilder()
+ .uri(URI.create("https://mywaifulist.moe/api/v1/" + param))
+ .timeout(Duration.ofSeconds(20))
+ .headers("Content-Type", "application/json", "apikey", apiKey)
+ .GET()
+ .build();
+ }
+
+ private HttpResponse buildHttpResponse(int statusCode, String body) {
+ return new HttpResponse<>() {
+ @Override
+ public int statusCode() {
+ return statusCode;
+ }
+
+ @Override
+ public HttpRequest request() {
+ return null;
+ }
+
+ @Override
+ public Optional> previousResponse() {
+ return Optional.empty();
+ }
+
+ @Override
+ public HttpHeaders headers() {
+ return null;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+
+ @Override
+ public Optional sslSession() {
+ return Optional.empty();
+ }
+
+ @Override
+ public URI uri() {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Version version() {
+ return null;
+ }
+ };
+ }
+}
+
diff --git a/src/test/java/me/goudham/util/TestEntity.java b/src/test/java/me/goudham/util/TestEntity.java
new file mode 100644
index 0000000..761a80c
--- /dev/null
+++ b/src/test/java/me/goudham/util/TestEntity.java
@@ -0,0 +1,513 @@
+package me.goudham.util;
+
+import me.goudham.domain.pagination.Links;
+import me.goudham.domain.pagination.Meta;
+import me.goudham.domain.pagination.PaginationData;
+import me.goudham.domain.series.FilteredSeries;
+import me.goudham.domain.series.Series;
+import me.goudham.domain.user.Creator;
+import me.goudham.domain.waifu.FilteredWaifu;
+import me.goudham.domain.waifu.Waifu;
+import me.goudham.domain.waifu.WaifuImage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestEntity {
+ public static Waifu getExpectedWaifu() {
+ Series series = new Series();
+ series.setName("Monster Musume: Everyday Life with Monster Girls");
+ series.setOriginalName(null);
+ series.setRomajiName(null);
+ series.setDescription("With his parents abroad, Kimihito Kurusu lived a quiet, unremarkable life alone until monster girls came crowding in! This alternate reality presents cutting-edge Japan, the first country to promote the integration of non-human species into society. After the incompetence of interspecies exchange coordinator Agent Smith leaves Kimihito as the homestay caretaker of a Lamia named Miia, the newly-minted \"Darling\" quickly attracts girls of various breeds, resulting in an ever-growing harem flush with eroticism and attraction.\r\n" +
+ "\r\n" +
+ "Unfortunately for him and the ladies, sexual interactions between species is forbidden by the Interspecies Exchange Act! The only loophole is through an experimental marriage provision. Kimihito's life becomes fraught with an abundance of creature-specific caveats and sensitive interspecies law as the passionate, affectionate, and lusty women hound his every move, seeking his romantic and sexual affections. With new species often appearing and events materializing out of thin air, where Kimihito and his harem go is anyone's guess!\r\n" +
+ "\r\n" +
+ "[Written by MAL Rewrite]");
+ series.setSlug("monster-musume-everyday-life-with-monster-girls");
+ series.setReleaseDate(null);
+ series.setAiringEnd(null);
+ series.setAiringStart(null);
+ series.setEpisodeCount(12);
+ series.setDisplayPicture("https://thicc.mywaifulist.moe/series/201/29eb0d9393f2f281e2882b08d3b2af81577a3033a3544189b325b22f9659eba4.jpeg");
+ series.setUrl(null);
+ series.setStudio(null);
+ series.setType(null);
+ series.setId(null);
+
+ List appearances = new ArrayList<>();
+ appearances.add(series);
+
+ Creator creator = new Creator();
+ creator.setId(1);
+ creator.setName("ReaverCelty");
+
+ Waifu waifu = new Waifu();
+ waifu.setId(1);
+ waifu.setSlug("rachnera-arachnera-monster-musume-everyday-life-with-monster-girls");
+ waifu.setCreator(creator);
+ waifu.setName("Rachnera Arachnera");
+ waifu.setOriginalName("ラクネラ・アラクネラ");
+ waifu.setRomajiName("Rakunera Arakunera");
+ waifu.setDisplayPicture("https://thicc.mywaifulist.moe/waifus/1/5c1be30a7b7834faef76e4b4dd8a512030a0cd3437f6a5bcc75de1ac33908092_thumb.jpeg");
+ waifu.setDescription("Rachnera Arachnera is an Arachne and the sixth girl to move in with Kimihito and fifth girl to officially do so. Due to their fear of her, her original host family had sold her off to Kasegi, who used her and her webbing to make money. This, along with his cruel behavior left her with a deep hatred for humans. This behaviour changed after she abducted Kurusu and found out that he was a decent person who didn't discriminate against non-humans. Since then, Rachnera has been flirting with Kurusu while facing her fellow monster girls in the house, especially Miia and Centorea Shianus who see her as their love rival for Kurusu.\n" +
+ "\n" +
+ "Rachnera has the body of a young woman with short lavender hair that covers the right side of her face. She has six pupil-less, monochromatic red eyes, and sharp and pointed teeth. Her most noticeable physical trait is the mass located at her humanoid buttocks which is that of a giant spider adorned with a large skull design on the back of its abdomen. She has black carapace covering her arms from the shoulders down, and long fingers with gauntlet-like plating on her hands that end in sharpened points. Her usual attire consists of a revealing halter top with shoulder-less sleeves, and black and gold loin cloth with a slightly-frilly white trim. Her outfits change more often than most of the other girls and are usually more revealing or mature.\n" +
+ "\n" +
+ "Most of Rachnera's personality seems to be determined by her bad experiences with humans. Because of this, she initially hated humans, and believed them all to be hypocrites. While she believed this by her outlook of Kimihito as well, she was shocked to find out his kindness was genuine. This changed her outlook significantly, though it did not completely change her. Nevertheless, Rachnera still retains at least some of her cynicism as evidenced by her conviction that Kimhito, as a man, would inevitably cheat on his home-stays. One of the biggest impacts that Rachnera's past has had on her is a deep-rooted hatred of dishonesty in any shape or form. No matter how ugly the truth is, Rachnera would still prefer it over pleasant lies. This is demonstrated when she managed to goad Cerea into admitting she hates her. Rachnera was actually happy with this, despite Cerea standing up for her to Miia earlier. Rachnera told Cerea that if she dislikes her then she should do so openly. One way to make her genuinely angry is to mock her history and motivations. Rachnera is extremely mischievous as evidenced when she gleefully traps her former host in her webbing after scolding her for not keeping her webs in one place . Likewise, she greatly enjoys provoking others, displaying an innate talent for saying the right thing at the right time to cause cracks in almost anyone's composure. In this respect, Miia and Centorea make very easy targets for her due to their mutual dislike for her and highly sensitive personalities. Rachnera also displays a pronounced sadistic streak given how she subjects Lilith to sexual torture and made members of her host family \"subs\" in her BDSM routines. . Notwithstanding her frightening appearance and unsettling \"hobbies\", Rachnera possesses a very sophisticated and seductive personality. Of all of Kimihito's tenants, she consistently displays the most confidence and self-awareness in her sex appeal when flirting with Kimihito (particularly when compared with Miia's suffocating displays of affection and Cerea's bashful prudishness). Likewise, Rachnera is the most licentious of the homestays as evidenced by her conscious attempts to sexually arouse Kimihito, along with her unabashed enjoyment of acts of (accidental) perversity on Kimihito's part . From what is shown, Rachnera greatly enjoys bondage, getting a great kick from tying her victims up in her own webbing, both sexually and non-sexually. However, dealing with both Papi and Suu in the others' absence proved to be too exhausting even for her. She is also revealed to be somewhat obsessive about perfecting her bondage technique, which annoys most of the other monster girls (except Suu, who cannot be bound, and Mero, who rather shamefully enjoys it) to varying degrees , though she seems to have exempted Kimihito from being webbed up in this fashion. However, she seems to include every monster girl who lives in Kimihito's home or is a possible rival as fair game, a fact that has Lala more than a little scared of her. She is implied to be somewhat lazy, and can frequently be seen slacking off. However, even despite her sinister traits, it is clear Rachnera's the most mature of all the home-stays, and can quickly discard her playful attitude and become very serious if the situation calls for it. Most notably, Papi and Suu consider Rachnera to be their favorite teacher in the household, even more than Kimihito himself, due to Rachnera being the most knowledgeable and skilled at teaching. Ironically, despite being built like a dangerous predator, Rachnera has great self-restraint, and Kimihito even noted that she's the only member of the household who has never genuinely harmed or endangered him, even by accident, which caused to her to become embarrassed. It is also shown that she cares deeply for Kimihito as when Lala said that Kimihito was going to die, she angrily bound her in thread and threatened her not to speak so offensively in front of her. Despite all this, according to Lilith, Rachnera's strong personality is merely a façade she puts up. Rachnera pretends to be uncaring, however she is afraid of being rejected again for her features, and that she is very self-conscious about her appearance. Rachnera's angry reaction implies that there is some truth to those words. In fact, when Rachnera gets intoxicated, she becomes much more emotional and honest about her true feelings.");
+ waifu.setWeight(82.0);
+ waifu.setHeight(198.0);
+ waifu.setBust(92.0);
+ waifu.setHip(87.0);
+ waifu.setWaist(55.0);
+ waifu.setBloodType(null);
+ waifu.setOrigin(null);
+ waifu.setAge(null);
+ waifu.setBirthdayMonth("June");
+ waifu.setBirthdayDay(27);
+ waifu.setBirthdayYear(null);
+ waifu.setLikes(1282);
+ waifu.setTrash(212);
+ waifu.setUrl("https://www.mywaifulist.moe/waifu/rachnera-arachnera-monster-musume-everyday-life-with-monster-girls");
+ waifu.setHusbando(false);
+ waifu.setNsfw(false);
+ waifu.setPopularityRank(173);
+ waifu.setLikeRank(159);
+ waifu.setTrashRank(244);
+ waifu.setAppearances(appearances);
+ waifu.setSeries(series);
+
+ return waifu;
+ }
+
+ public static List getBestWaifus() {
+ List bestWaifus = new ArrayList<>();
+
+ List appearances1 = new ArrayList<>();
+ appearances1.add(createFilteredSeries("When They Cry", "when-they-cry", "https://www.mywaifulist.moe/series/when-they-cry"));
+ appearances1.add(createFilteredSeries("When They Cry (2020)", "when-they-cry-2020", "https://www.mywaifulist.moe/series/when-they-cry-2020"));
+ appearances1.add(createFilteredSeries("When They Cry - Sotsu", "when-they-cry-sotsu", "https://www.mywaifulist.moe/series/when-they-cry-sotsu"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances1,
+ "Rika is one of the main characters in Higurashi no Naku Koro ni Kai. She is...",
+ "https://thicc.mywaifulist.moe/waifus/730/4aa77bd6a64aff6504cd2f8e718040b72b9f010a4b8124968cc3f2872904b3bb_thumb.jpeg",
+ 730.0,
+ 373,
+ "Rika Furude",
+ "古手 梨花",
+ null,
+ null,
+ "rika-furude-when-they-cry",
+ 42,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/rika-furude-when-they-cry",
+ 1
+ )
+ );
+
+ List appearances2 = new ArrayList<>();
+ appearances2.add(createFilteredSeries("That Time I Got Reincarnated as a Slime", "that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"));
+ appearances2.add(createFilteredSeries("That Time I Got Reincarnated as a Slime OVA", "that-time-i-got-reincarnated-as-a-slime-ova", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"));
+ appearances2.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2", "that-time-i-got-reincarnated-as-a-slime-season-2", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"));
+ appearances2.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2: Part II", "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"));
+ appearances2.add(createFilteredSeries("The Slime Diaries: That Time I Got Reincarnated as a Slime", "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances2,
+ "Shion is one of the third group of subordinates to be named by Rimuru, and...",
+ "https://thicc.mywaifulist.moe/waifus/7727/bfaed60aca345dcf1937fe4a395112c9748f9712cbad7e4d1ab95faca9480f31_thumb.jpeg",
+ 7727.0,
+ 727,
+ "Shion",
+ "シオン",
+ null,
+ null,
+ "shion-that-time-i-got-reincarnated-as-a-slime",
+ 70,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/shion-that-time-i-got-reincarnated-as-a-slime",
+ 1
+ )
+ );
+
+ List appearances3 = new ArrayList<>();
+ appearances3.add(createFilteredSeries("That Time I Got Reincarnated as a Slime", "that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"));
+ appearances3.add(createFilteredSeries("That Time I Got Reincarnated as a Slime OVA", "that-time-i-got-reincarnated-as-a-slime-ova", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"));
+ appearances3.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2", "that-time-i-got-reincarnated-as-a-slime-season-2", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"));
+ appearances3.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2: Part II", "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"));
+ appearances3.add(createFilteredSeries("The Slime Diaries: That Time I Got Reincarnated as a Slime", "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances3,
+ "Milim Nava is one of the oldest and strongest Demon Lords, and the third Tr...",
+ "https://thicc.mywaifulist.moe/waifus/21495/788959e5be43dcea863ea095e7380b18e330f98d98ea21ae799b68a02c4972a7_thumb.jpeg",
+ 7728.0,
+ 1016,
+ "Milim Nava",
+ "ミリム・ナーヴァ",
+ null,
+ null,
+ "milim-nava-that-time-i-got-reincarnated-as-a-slime",
+ 85,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/milim-nava-that-time-i-got-reincarnated-as-a-slime",
+ 1
+ )
+ );
+
+ List appearances4 = new ArrayList<>();
+ appearances4.add(createFilteredSeries("That Time I Got Reincarnated as a Slime", "that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"));
+ appearances4.add(createFilteredSeries("That Time I Got Reincarnated as a Slime OVA", "that-time-i-got-reincarnated-as-a-slime-ova", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"));
+ appearances4.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2", "that-time-i-got-reincarnated-as-a-slime-season-2", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"));
+ appearances4.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2: Part II", "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"));
+ appearances4.add(createFilteredSeries("The Slime Diaries: That Time I Got Reincarnated as a Slime", "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances4,
+ "Shuna is one of the third group of subordinate Monsters to be named by Rimu...",
+ "https://thicc.mywaifulist.moe/waifus/7729/e1f93cba40cb61513793d21a6f2772f849485d7cb6402b07a80b716e099faed8_thumb.jpeg",
+ 7729.0,
+ 998,
+ "Shuna",
+ "朱菜",
+ "Shuna",
+ "Shuna",
+ "shuna-that-time-i-got-reincarnated-as-a-slime",
+ 78,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/shuna-that-time-i-got-reincarnated-as-a-slime",
+ 1
+ )
+ );
+
+ List appearances5 = new ArrayList<>();
+ appearances5.add(createFilteredSeries("That Time I Got Reincarnated as a Slime", "that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"));
+ appearances5.add(createFilteredSeries("That Time I Got Reincarnated as a Slime OVA", "that-time-i-got-reincarnated-as-a-slime-ova", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"));
+ appearances5.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2", "that-time-i-got-reincarnated-as-a-slime-season-2", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"));
+ appearances5.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2: Part II", "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"));
+ appearances5.add(createFilteredSeries("The Slime Diaries: That Time I Got Reincarnated as a Slime", "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances5,
+ "The main protagonist of the series and Founder and King of the city known a...",
+ "https://thicc.mywaifulist.moe/waifus/7730/a5c4a0dcc0f01851f6df0c551ca4e00df3af50b7d65342e02e994f2ecf62b7a8_thumb.jpeg",
+ 7730.0,
+ 550,
+ "Rimuru Tempest",
+ "リムル=テンペスト",
+ null,
+ null,
+ "rimuru-tempest-that-time-i-got-reincarnated-as-a-slime",
+ 55,
+ "Husbando",
+ "https://www.mywaifulist.moe/waifu/rimuru-tempest-that-time-i-got-reincarnated-as-a-slime",
+ 1
+ )
+ );
+
+ List appearances6 = new ArrayList<>();
+ appearances6.add(createFilteredSeries("That Time I Got Reincarnated as a Slime", "that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"));
+ appearances6.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2", "that-time-i-got-reincarnated-as-a-slime-season-2", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"));
+ appearances6.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2: Part II", "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"));
+ appearances6.add(createFilteredSeries("The Slime Diaries: That Time I Got Reincarnated as a Slime", "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances6,
+ "Shizue Izawa, also known as simply Shizu, was the companion of the previous...",
+ "https://thicc.mywaifulist.moe/waifus/7731/c89f6d88d65ec0358babb3b35696d2648178bf745b0bb1431167f3a97d2ccd7d_thumb.jpeg",
+ 7731.0,
+ 914,
+ "Shizue Izawa",
+ "井沢静江",
+ "Izawa Shizue",
+ "Izawa Shizue",
+ "shizue-izawa-that-time-i-got-reincarnated-as-a-slime",
+ 81,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/shizue-izawa-that-time-i-got-reincarnated-as-a-slime",
+ 1
+ )
+ );
+
+ List appearances7 = new ArrayList<>();
+ appearances7.add(createFilteredSeries("Magia Record: Puella Magi Madoka Magica Side Story", "magia-record-puella-magi-madoka-magica-side-story", "https://www.mywaifulist.moe/series/magia-record-puella-magi-madoka-magica-side-story"));
+ appearances7.add(createFilteredSeries("Magia Record: Puella Magi Madoka Magica Side Story 2nd Season", "magia-record-puella-magi-madoka-magica-side-story-2nd-season", "https://www.mywaifulist.moe/series/magia-record-puella-magi-madoka-magica-side-story-2nd-season"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances7,
+ "A genius artist obsessed with the theme of magical girl life and death, who...",
+ "https://thicc.mywaifulist.moe/waifus/16490/fbc5dab5ec0381973c837261ec8f6bfa93cebe8a7471ece03627c754442f9965_thumb.png",
+ 16490.0,
+ 44,
+ "Alina Gray",
+ "アリナ グレイ",
+ null,
+ null,
+ "alina-gray",
+ 1,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/alina-gray",
+ 1
+ )
+ );
+
+ List appearances8 = new ArrayList<>();
+ appearances8.add(createFilteredSeries("The Great Jahy Will Not Be Defeated!", "the-great-jahy-will-not-be-defeated", "https://www.mywaifulist.moe/series/the-great-jahy-will-not-be-defeated"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances8,
+ "The #2 of the Demon Realm... well, was",
+ "https://thicc.mywaifulist.moe/waifus/18772/6d44ea024aff2a387319065b4c37a2f6eeab72deecfc5a308b2bb42a796d8bff_thumb.jpeg",
+ 18772.0,
+ 126,
+ "Jahy",
+ "ジャヒ",
+ null,
+ null,
+ "jahy",
+ 4,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/jahy",
+ 1
+ )
+ );
+
+ List appearances9 = new ArrayList<>();
+ appearances9.add(createFilteredSeries("That Time I Got Reincarnated as a Slime", "that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"));
+ appearances9.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2", "that-time-i-got-reincarnated-as-a-slime-season-2", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"));
+ appearances9.add(createFilteredSeries("That Time I Got Reincarnated as a Slime Season 2: Part II", "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii", "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"));
+ appearances9.add(createFilteredSeries("The Slime Diaries: That Time I Got Reincarnated as a Slime", "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime", "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances9,
+ "Treyni (トレイニー Toreinii) is a dryad that resides in the Jura Forest....",
+ "https://thicc.mywaifulist.moe/waifus/19048/f624b96166f76c3e1d5fc720ca93713e0d491fbae1634373e540d5cdd73dbcd0_thumb.png",
+ 19048.0,
+ 187,
+ "Treyni",
+ "トレイニー Protector of Treants Manager of the Great Jura Forest",
+ null,
+ null,
+ "treyni-that-time-i-got-reincarnated-as-a-slime",
+ 16,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/treyni-that-time-i-got-reincarnated-as-a-slime",
+ 1
+ )
+ );
+
+ List appearances10 = new ArrayList<>();
+ appearances10.add(createFilteredSeries("Obey Me!", "obey-me", "https://www.mywaifulist.moe/series/obey-me"));
+
+ bestWaifus.add(createFilteredWaifu(
+ appearances10,
+ "Satan is the youngest member of the family but is ranked 4th due to his pow...",
+ "https://thicc.mywaifulist.moe/waifus/31538/5a5889850922830d32dcc800d88acc4e858c19ad7e8f30b53df9f1b3963ea068_thumb.png",
+ 31538.0,
+ 36,
+ "Satan",
+ "サタン",
+ "",
+ "",
+ "satan-2",
+ 0,
+ "Husbando",
+ "https://www.mywaifulist.moe/waifu/satan-2",
+ 1
+ )
+ );
+
+ return bestWaifus;
+ }
+
+ public static PaginationData getWaifuImages() {
+ PaginationData waifuImagesPaginationData = new PaginationData<>();
+
+ Meta meta = new Meta();
+ meta.setCurrentPage(1);
+ meta.setFrom(1);
+ meta.setLastPage(13);
+ meta.setPath("https://mywaifulist.moe/api/v1/waifu/1/images");
+ meta.setTo(10);
+ meta.setPerPage(10);
+ meta.setTotal(123);
+
+ waifuImagesPaginationData.setMeta(meta);
+
+ Links links = new Links();
+ links.setFirst("https://mywaifulist.moe/api/v1/waifu/1/images?page=1");
+ links.setLast("https://mywaifulist.moe/api/v1/waifu/1/images?page=13");
+ links.setNext("https://mywaifulist.moe/api/v1/waifu/1/images?page=2");
+ links.setPrev(null);
+
+ waifuImagesPaginationData.setLinks(links);
+
+ List waifuImages = new ArrayList<>();
+
+ waifuImages.add(createWaifuImage(
+ 188451,
+ "https://thicc.mywaifulist.moe/waifus/1/5c1be30a7b7834faef76e4b4dd8a512030a0cd3437f6a5bcc75de1ac33908092.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/5c1be30a7b7834faef76e4b4dd8a512030a0cd3437f6a5bcc75de1ac33908092_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 178162,
+ "https://thicc.mywaifulist.moe/waifus/1/99568b61823e34d6935926d5a589181926cb83f4c4682f5b22f299f940da7503.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/99568b61823e34d6935926d5a589181926cb83f4c4682f5b22f299f940da7503_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164424,
+ "https://thicc.mywaifulist.moe/waifus/1/c6d56e30ef20371df87f8967a10192140c71f03b39238c252fe6f0d49a03c1ca.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/c6d56e30ef20371df87f8967a10192140c71f03b39238c252fe6f0d49a03c1ca_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164423,
+ "https://thicc.mywaifulist.moe/waifus/1/613ea8485d61fe89e52ea4a1865dff7bcf59f9d5b0d4640b78fce7778d25b9ae.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/613ea8485d61fe89e52ea4a1865dff7bcf59f9d5b0d4640b78fce7778d25b9ae_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164421,
+ "https://thicc.mywaifulist.moe/waifus/1/53675eb34be747c6ff4a1004da5bedc8a8d0baf435df7966404f5c29b8bf92ae.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/53675eb34be747c6ff4a1004da5bedc8a8d0baf435df7966404f5c29b8bf92ae_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164422,
+ "https://thicc.mywaifulist.moe/waifus/1/18f67d18effe733f394c643fbf9735efbfe955497d5bcddb984ba78f66718f5c.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/18f67d18effe733f394c643fbf9735efbfe955497d5bcddb984ba78f66718f5c_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164419,
+ "https://thicc.mywaifulist.moe/waifus/1/de2eca1b6ffaa99134427eef444edf84e5274dc1812b014a70871e39eb380ef8.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/de2eca1b6ffaa99134427eef444edf84e5274dc1812b014a70871e39eb380ef8_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164420,
+ "https://thicc.mywaifulist.moe/waifus/1/e52536c04ebf17ac73bfa45526be95b2271e23802740591c0455e1d99f1b0f09.png",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/e52536c04ebf17ac73bfa45526be95b2271e23802740591c0455e1d99f1b0f09_thumb.png"
+ ));
+ waifuImages.add(createWaifuImage(
+ 164418,
+ "https://thicc.mywaifulist.moe/waifus/1/9662ce327814ceb88fd77dbfa1456dfce2b6addcf2d7bb0f9b335a35136f1d33.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/9662ce327814ceb88fd77dbfa1456dfce2b6addcf2d7bb0f9b335a35136f1d33_thumb.jpeg"
+ ));
+ waifuImages.add(createWaifuImage(
+ 113519,
+ "https://thicc.mywaifulist.moe/waifus/1/2543fd22eafb15ec6e8810c11ce25161f058bbb9315e05845f593bbb4f26f859.jpeg",
+ "false",
+ "https://thicc.mywaifulist.moe/waifus/1/2543fd22eafb15ec6e8810c11ce25161f058bbb9315e05845f593bbb4f26f859_thumb.jpeg"
+ ));
+
+ waifuImagesPaginationData.setData(waifuImages);
+
+ return waifuImagesPaginationData;
+ }
+
+ public static List getBetaSearch() {
+ List betaSearchResults = new ArrayList<>();
+
+ List appearances = new ArrayList<>();
+ appearances.add(createFullFilteredSeries(
+ "Unlike many schools, attending Hyakkaou Private Academy prepares students f...",
+ "https://thicc.mywaifulist.moe/series/1680/13c030778b8e7e44ddccb8ca999e18d6ea2b3d547eacc1f63bf5849ade858bb0.jpeg",
+ 1680,
+ "Kakegurui: Compulsive Gambler",
+ "Kakegurui",
+ 1,
+ null,
+ "kakegurui-compulsive-gambler",
+ "TV",
+ "https://www.mywaifulist.moe/series/kakegurui-compulsive-gambler"
+ ));
+
+ betaSearchResults.add(createFilteredWaifu(
+ appearances,
+ "Yumeko Jabami is the main protagonist of Kakegurui. She's a transfer studen...",
+ "https://thicc.mywaifulist.moe/waifus/6167/7bf292a1552161de90ff7d0c1eab2c890b2aa374903773d38d39c6c6e8d6f3d2_thumb.jpeg",
+ 6167.0,
+ 1889,
+ "Yumeko Jabami",
+ "蛇喰夢子",
+ "Jabami Yumeko",
+ "Jabami Yumeko",
+ "yumeko-jabami-kakegurui-compulsive-gambler",
+ 172,
+ "Waifu",
+ "https://www.mywaifulist.moe/waifu/yumeko-jabami-kakegurui-compulsive-gambler",
+ 2
+ )
+ );
+
+ return betaSearchResults;
+ }
+
+ private static FilteredSeries createFullFilteredSeries(String description, String displayPicture, int id, String name, String originalName, int relevance, String romajiName, String slug, String type, String url) {
+ FilteredSeries filteredSeries = new FilteredSeries();
+ filteredSeries.setDescription(description);
+ filteredSeries.setDisplayPicture(displayPicture);
+ filteredSeries.setId(id);
+ filteredSeries.setName(name);
+ filteredSeries.setOriginalName(originalName);
+ filteredSeries.setRelevance(relevance);
+ filteredSeries.setRomajiName(romajiName);
+ filteredSeries.setSlug(slug);
+ filteredSeries.setType(type);
+ filteredSeries.setUrl(url);
+ return filteredSeries;
+ }
+
+ private static WaifuImage createWaifuImage(Integer id, String image, String nsfw, String thumbnail) {
+ WaifuImage waifuImage = new WaifuImage();
+ waifuImage.setId(id);
+ waifuImage.setImage(image);
+ waifuImage.setNsfw(nsfw);
+ waifuImage.setThumbnail(thumbnail);
+ return waifuImage;
+ }
+
+ private static FilteredWaifu createFilteredWaifu(List appearances, String description, String displayPicture,
+ Double id, Integer likes, String name, String originalName, String romaji,
+ String romaji_name, String slug, Integer trash, String type, String url, Integer relevance) {
+ FilteredWaifu filteredWaifu = new FilteredWaifu();
+ filteredWaifu.setAppearances(appearances);
+ filteredWaifu.setDescription(description);
+ filteredWaifu.setDisplayPicture(displayPicture);
+ filteredWaifu.setId(id);
+ filteredWaifu.setLikes(likes);
+ filteredWaifu.setName(name);
+ filteredWaifu.setOriginalName(originalName);
+ filteredWaifu.setRomaji(romaji);
+ filteredWaifu.setRomajiName(romaji_name);
+ filteredWaifu.setSlug(slug);
+ filteredWaifu.setTrash(trash);
+ filteredWaifu.setType(type);
+ filteredWaifu.setUrl(url);
+ filteredWaifu.setRelevance(relevance);
+ return filteredWaifu;
+ }
+
+
+ private static FilteredSeries createFilteredSeries(String name, String slug, String url) {
+ FilteredSeries filteredSeries = new FilteredSeries();
+ filteredSeries.setName(name);
+ filteredSeries.setDescription(null);
+ filteredSeries.setDisplayPicture(null);
+ filteredSeries.setId(null);
+ filteredSeries.setOriginalName(null);
+ filteredSeries.setRelevance(1);
+ filteredSeries.setRomajiName(null);
+ filteredSeries.setType(null);
+ filteredSeries.setSlug(slug);
+ filteredSeries.setUrl(url);
+ filteredSeries.setRelevance(1);
+ return filteredSeries;
+ }
+}
diff --git a/src/test/resources/betaSearch.json b/src/test/resources/betaSearch.json
new file mode 100644
index 0000000..4555059
--- /dev/null
+++ b/src/test/resources/betaSearch.json
@@ -0,0 +1,34 @@
+{
+ "data": [
+ {
+ "appearances": [
+ {
+ "description": "Unlike many schools, attending Hyakkaou Private Academy prepares students f...",
+ "display_picture": "https://thicc.mywaifulist.moe/series/1680/13c030778b8e7e44ddccb8ca999e18d6ea2b3d547eacc1f63bf5849ade858bb0.jpeg",
+ "id": 1680,
+ "name": "Kakegurui: Compulsive Gambler",
+ "original_name": "Kakegurui",
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "kakegurui-compulsive-gambler",
+ "type": "TV",
+ "url": "https://www.mywaifulist.moe/series/kakegurui-compulsive-gambler"
+ }
+ ],
+ "description": "Yumeko Jabami is the main protagonist of Kakegurui. She's a transfer studen...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/6167/7bf292a1552161de90ff7d0c1eab2c890b2aa374903773d38d39c6c6e8d6f3d2_thumb.jpeg",
+ "id": 6167,
+ "likes": 1889,
+ "name": "Yumeko Jabami",
+ "original_name": "蛇喰夢子",
+ "relevance": 2,
+ "romaji": "Jabami Yumeko",
+ "romaji_name": "Jabami Yumeko",
+ "series": null,
+ "slug": "yumeko-jabami-kakegurui-compulsive-gambler",
+ "trash": 172,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/yumeko-jabami-kakegurui-compulsive-gambler"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/getBestWaifus.json b/src/test/resources/getBestWaifus.json
new file mode 100644
index 0000000..a820e47
--- /dev/null
+++ b/src/test/resources/getBestWaifus.json
@@ -0,0 +1,604 @@
+{
+ "data": [
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "When They Cry",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "when-they-cry",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/when-they-cry"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "When They Cry (2020)",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "when-they-cry-2020",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/when-they-cry-2020"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "When They Cry - Sotsu",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "when-they-cry-sotsu",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/when-they-cry-sotsu"
+ }
+ ],
+ "description": "Rika is one of the main characters in Higurashi no Naku Koro ni Kai. She is...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/730/4aa77bd6a64aff6504cd2f8e718040b72b9f010a4b8124968cc3f2872904b3bb_thumb.jpeg",
+ "id": 730,
+ "likes": 373,
+ "name": "Rika Furude",
+ "original_name": "古手 梨花",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "rika-furude-when-they-cry",
+ "trash": 42,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/rika-furude-when-they-cry"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime OVA",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-ova",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2: Part II",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Slime Diaries: That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"
+ }
+ ],
+ "description": "Shion is one of the third group of subordinates to be named by Rimuru, and...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/7727/bfaed60aca345dcf1937fe4a395112c9748f9712cbad7e4d1ab95faca9480f31_thumb.jpeg",
+ "id": 7727,
+ "likes": 727,
+ "name": "Shion",
+ "original_name": "シオン",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "shion-that-time-i-got-reincarnated-as-a-slime",
+ "trash": 70,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/shion-that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime OVA",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-ova",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2: Part II",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Slime Diaries: That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"
+ }
+ ],
+ "description": "Milim Nava is one of the oldest and strongest Demon Lords, and the third Tr...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/21495/788959e5be43dcea863ea095e7380b18e330f98d98ea21ae799b68a02c4972a7_thumb.jpeg",
+ "id": 7728,
+ "likes": 1016,
+ "name": "Milim Nava",
+ "original_name": "ミリム・ナーヴァ",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "milim-nava-that-time-i-got-reincarnated-as-a-slime",
+ "trash": 85,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/milim-nava-that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime OVA",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-ova",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2: Part II",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Slime Diaries: That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"
+ }
+ ],
+ "description": "Shuna is one of the third group of subordinate Monsters to be named by Rimu...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/7729/e1f93cba40cb61513793d21a6f2772f849485d7cb6402b07a80b716e099faed8_thumb.jpeg",
+ "id": 7729,
+ "likes": 998,
+ "name": "Shuna",
+ "original_name": "朱菜",
+ "relevance": 1,
+ "romaji": "Shuna",
+ "romaji_name": "Shuna",
+ "series": null,
+ "slug": "shuna-that-time-i-got-reincarnated-as-a-slime",
+ "trash": 78,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/shuna-that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime OVA",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-ova",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-ova"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2: Part II",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Slime Diaries: That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"
+ }
+ ],
+ "description": "The main protagonist of the series and Founder and King of the city known a...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/7730/a5c4a0dcc0f01851f6df0c551ca4e00df3af50b7d65342e02e994f2ecf62b7a8_thumb.jpeg",
+ "id": 7730,
+ "likes": 550,
+ "name": "Rimuru Tempest",
+ "original_name": "リムル=テンペスト",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "rimuru-tempest-that-time-i-got-reincarnated-as-a-slime",
+ "trash": 55,
+ "type": "Husbando",
+ "url": "https://www.mywaifulist.moe/waifu/rimuru-tempest-that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2: Part II",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Slime Diaries: That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"
+ }
+ ],
+ "description": "Shizue Izawa, also known as simply Shizu, was the companion of the previous...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/7731/c89f6d88d65ec0358babb3b35696d2648178bf745b0bb1431167f3a97d2ccd7d_thumb.jpeg",
+ "id": 7731,
+ "likes": 914,
+ "name": "Shizue Izawa",
+ "original_name": "井沢静江",
+ "relevance": 1,
+ "romaji": "Izawa Shizue",
+ "romaji_name": "Izawa Shizue",
+ "series": null,
+ "slug": "shizue-izawa-that-time-i-got-reincarnated-as-a-slime",
+ "trash": 81,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/shizue-izawa-that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "Magia Record: Puella Magi Madoka Magica Side Story",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "magia-record-puella-magi-madoka-magica-side-story",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/magia-record-puella-magi-madoka-magica-side-story"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "Magia Record: Puella Magi Madoka Magica Side Story 2nd Season",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "magia-record-puella-magi-madoka-magica-side-story-2nd-season",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/magia-record-puella-magi-madoka-magica-side-story-2nd-season"
+ }
+ ],
+ "description": "A genius artist obsessed with the theme of magical girl life and death, who...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/16490/fbc5dab5ec0381973c837261ec8f6bfa93cebe8a7471ece03627c754442f9965_thumb.png",
+ "id": 16490,
+ "likes": 44,
+ "name": "Alina Gray",
+ "original_name": "アリナ グレイ",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "alina-gray",
+ "trash": 1,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/alina-gray"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Great Jahy Will Not Be Defeated!",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-great-jahy-will-not-be-defeated",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-great-jahy-will-not-be-defeated"
+ }
+ ],
+ "description": "The #2 of the Demon Realm... well, was",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/18772/6d44ea024aff2a387319065b4c37a2f6eeab72deecfc5a308b2bb42a796d8bff_thumb.jpeg",
+ "id": 18772,
+ "likes": 126,
+ "name": "Jahy",
+ "original_name": "ジャヒ",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "jahy",
+ "trash": 4,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/jahy"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "That Time I Got Reincarnated as a Slime Season 2: Part II",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "that-time-i-got-reincarnated-as-a-slime-season-2-part-ii",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/that-time-i-got-reincarnated-as-a-slime-season-2-part-ii"
+ },
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "The Slime Diaries: That Time I Got Reincarnated as a Slime",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "the-slime-diaries-that-time-i-got-reincarnated-as-a-slime",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/the-slime-diaries-that-time-i-got-reincarnated-as-a-slime"
+ }
+ ],
+ "description": "Treyni (トレイニー Toreinii) is a dryad that resides in the Jura Forest....",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/19048/f624b96166f76c3e1d5fc720ca93713e0d491fbae1634373e540d5cdd73dbcd0_thumb.png",
+ "id": 19048,
+ "likes": 187,
+ "name": "Treyni",
+ "original_name": "トレイニー Protector of Treants Manager of the Great Jura Forest",
+ "relevance": 1,
+ "romaji": null,
+ "romaji_name": null,
+ "series": null,
+ "slug": "treyni-that-time-i-got-reincarnated-as-a-slime",
+ "trash": 16,
+ "type": "Waifu",
+ "url": "https://www.mywaifulist.moe/waifu/treyni-that-time-i-got-reincarnated-as-a-slime"
+ },
+ {
+ "appearances": [
+ {
+ "description": null,
+ "display_picture": null,
+ "id": null,
+ "name": "Obey Me!",
+ "original_name": null,
+ "relevance": 1,
+ "romaji_name": null,
+ "slug": "obey-me",
+ "type": null,
+ "url": "https://www.mywaifulist.moe/series/obey-me"
+ }
+ ],
+ "description": "Satan is the youngest member of the family but is ranked 4th due to his pow...",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/31538/5a5889850922830d32dcc800d88acc4e858c19ad7e8f30b53df9f1b3963ea068_thumb.png",
+ "id": 31538,
+ "likes": 36,
+ "name": "Satan",
+ "original_name": "サタン",
+ "relevance": 1,
+ "romaji": "",
+ "romaji_name": "",
+ "series": null,
+ "slug": "satan-2",
+ "trash": 0,
+ "type": "Husbando",
+ "url": "https://www.mywaifulist.moe/waifu/satan-2"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/getWaifu.json b/src/test/resources/getWaifu.json
new file mode 100644
index 0000000..11c4f3e
--- /dev/null
+++ b/src/test/resources/getWaifu.json
@@ -0,0 +1,71 @@
+{
+ "data": {
+ "age": null,
+ "appearances": [
+ {
+ "airing_end": null,
+ "airing_start": null,
+ "description": "With his parents abroad, Kimihito Kurusu lived a quiet, unremarkable life alone until monster girls came crowding in! This alternate reality presents cutting-edge Japan, the first country to promote the integration of non-human species into society. After the incompetence of interspecies exchange coordinator Agent Smith leaves Kimihito as the homestay caretaker of a Lamia named Miia, the newly-minted \"Darling\" quickly attracts girls of various breeds, resulting in an ever-growing harem flush with eroticism and attraction.\r\n\r\nUnfortunately for him and the ladies, sexual interactions between species is forbidden by the Interspecies Exchange Act! The only loophole is through an experimental marriage provision. Kimihito's life becomes fraught with an abundance of creature-specific caveats and sensitive interspecies law as the passionate, affectionate, and lusty women hound his every move, seeking his romantic and sexual affections. With new species often appearing and events materializing out of thin air, where Kimihito and his harem go is anyone's guess!\r\n\r\n[Written by MAL Rewrite]",
+ "display_picture": "https://thicc.mywaifulist.moe/series/201/29eb0d9393f2f281e2882b08d3b2af81577a3033a3544189b325b22f9659eba4.jpeg",
+ "episode_count": 12,
+ "name": "Monster Musume: Everyday Life with Monster Girls",
+ "original_name": null,
+ "release": null,
+ "romaji_name": null,
+ "slug": "monster-musume-everyday-life-with-monster-girls",
+ "studio": null
+ }
+ ],
+ "birthday_day": 27,
+ "birthday_month": "June",
+ "birthday_year": null,
+ "blood_type": null,
+ "bust": "92.00",
+ "creator": {
+ "id": 1,
+ "name": "ReaverCelty"
+ },
+ "description": "Rachnera Arachnera is an Arachne and the sixth girl to move in with Kimihito and fifth girl to officially do so. Due to their fear of her, her original host family had sold her off to Kasegi, who used her and her webbing to make money. This, along with his cruel behavior left her with a deep hatred for humans. This behaviour changed after she abducted Kurusu and found out that he was a decent person who didn't discriminate against non-humans. Since then, Rachnera has been flirting with Kurusu while facing her fellow monster girls in the house, especially Miia and Centorea Shianus who see her as their love rival for Kurusu.\n\nRachnera has the body of a young woman with short lavender hair that covers the right side of her face. She has six pupil-less, monochromatic red eyes, and sharp and pointed teeth. Her most noticeable physical trait is the mass located at her humanoid buttocks which is that of a giant spider adorned with a large skull design on the back of its abdomen. She has black carapace covering her arms from the shoulders down, and long fingers with gauntlet-like plating on her hands that end in sharpened points. Her usual attire consists of a revealing halter top with shoulder-less sleeves, and black and gold loin cloth with a slightly-frilly white trim. Her outfits change more often than most of the other girls and are usually more revealing or mature.\n\nMost of Rachnera's personality seems to be determined by her bad experiences with humans. Because of this, she initially hated humans, and believed them all to be hypocrites. While she believed this by her outlook of Kimihito as well, she was shocked to find out his kindness was genuine. This changed her outlook significantly, though it did not completely change her. Nevertheless, Rachnera still retains at least some of her cynicism as evidenced by her conviction that Kimhito, as a man, would inevitably cheat on his home-stays. One of the biggest impacts that Rachnera's past has had on her is a deep-rooted hatred of dishonesty in any shape or form. No matter how ugly the truth is, Rachnera would still prefer it over pleasant lies. This is demonstrated when she managed to goad Cerea into admitting she hates her. Rachnera was actually happy with this, despite Cerea standing up for her to Miia earlier. Rachnera told Cerea that if she dislikes her then she should do so openly. One way to make her genuinely angry is to mock her history and motivations. Rachnera is extremely mischievous as evidenced when she gleefully traps her former host in her webbing after scolding her for not keeping her webs in one place . Likewise, she greatly enjoys provoking others, displaying an innate talent for saying the right thing at the right time to cause cracks in almost anyone's composure. In this respect, Miia and Centorea make very easy targets for her due to their mutual dislike for her and highly sensitive personalities. Rachnera also displays a pronounced sadistic streak given how she subjects Lilith to sexual torture and made members of her host family \"subs\" in her BDSM routines. . Notwithstanding her frightening appearance and unsettling \"hobbies\", Rachnera possesses a very sophisticated and seductive personality. Of all of Kimihito's tenants, she consistently displays the most confidence and self-awareness in her sex appeal when flirting with Kimihito (particularly when compared with Miia's suffocating displays of affection and Cerea's bashful prudishness). Likewise, Rachnera is the most licentious of the homestays as evidenced by her conscious attempts to sexually arouse Kimihito, along with her unabashed enjoyment of acts of (accidental) perversity on Kimihito's part . From what is shown, Rachnera greatly enjoys bondage, getting a great kick from tying her victims up in her own webbing, both sexually and non-sexually. However, dealing with both Papi and Suu in the others' absence proved to be too exhausting even for her. She is also revealed to be somewhat obsessive about perfecting her bondage technique, which annoys most of the other monster girls (except Suu, who cannot be bound, and Mero, who rather shamefully enjoys it) to varying degrees , though she seems to have exempted Kimihito from being webbed up in this fashion. However, she seems to include every monster girl who lives in Kimihito's home or is a possible rival as fair game, a fact that has Lala more than a little scared of her. She is implied to be somewhat lazy, and can frequently be seen slacking off. However, even despite her sinister traits, it is clear Rachnera's the most mature of all the home-stays, and can quickly discard her playful attitude and become very serious if the situation calls for it. Most notably, Papi and Suu consider Rachnera to be their favorite teacher in the household, even more than Kimihito himself, due to Rachnera being the most knowledgeable and skilled at teaching. Ironically, despite being built like a dangerous predator, Rachnera has great self-restraint, and Kimihito even noted that she's the only member of the household who has never genuinely harmed or endangered him, even by accident, which caused to her to become embarrassed. It is also shown that she cares deeply for Kimihito as when Lala said that Kimihito was going to die, she angrily bound her in thread and threatened her not to speak so offensively in front of her. Despite all this, according to Lilith, Rachnera's strong personality is merely a façade she puts up. Rachnera pretends to be uncaring, however she is afraid of being rejected again for her features, and that she is very self-conscious about her appearance. Rachnera's angry reaction implies that there is some truth to those words. In fact, when Rachnera gets intoxicated, she becomes much more emotional and honest about her true feelings.",
+ "display_picture": "https://thicc.mywaifulist.moe/waifus/1/5c1be30a7b7834faef76e4b4dd8a512030a0cd3437f6a5bcc75de1ac33908092_thumb.jpeg",
+ "height": "198.00",
+ "hip": "87.00",
+ "husbando": false,
+ "id": 1,
+ "like_rank": 159,
+ "likes": 1282,
+ "name": "Rachnera Arachnera",
+ "nsfw": false,
+ "origin": null,
+ "original_name": "ラクネラ・アラクネラ",
+ "popularity_rank": 173,
+ "romaji_name": "Rakunera Arakunera",
+ "series": {
+ "airing_end": null,
+ "airing_start": null,
+ "description": "With his parents abroad, Kimihito Kurusu lived a quiet, unremarkable life alone until monster girls came crowding in! This alternate reality presents cutting-edge Japan, the first country to promote the integration of non-human species into society. After the incompetence of interspecies exchange coordinator Agent Smith leaves Kimihito as the homestay caretaker of a Lamia named Miia, the newly-minted \"Darling\" quickly attracts girls of various breeds, resulting in an ever-growing harem flush with eroticism and attraction.\r\n\r\nUnfortunately for him and the ladies, sexual interactions between species is forbidden by the Interspecies Exchange Act! The only loophole is through an experimental marriage provision. Kimihito's life becomes fraught with an abundance of creature-specific caveats and sensitive interspecies law as the passionate, affectionate, and lusty women hound his every move, seeking his romantic and sexual affections. With new species often appearing and events materializing out of thin air, where Kimihito and his harem go is anyone's guess!\r\n\r\n[Written by MAL Rewrite]",
+ "display_picture": "https://thicc.mywaifulist.moe/series/201/29eb0d9393f2f281e2882b08d3b2af81577a3033a3544189b325b22f9659eba4.jpeg",
+ "episode_count": 12,
+ "name": "Monster Musume: Everyday Life with Monster Girls",
+ "original_name": null,
+ "release": null,
+ "romaji_name": null,
+ "slug": "monster-musume-everyday-life-with-monster-girls",
+ "studio": null
+ },
+ "slug": "rachnera-arachnera-monster-musume-everyday-life-with-monster-girls",
+ "tags": [
+ {
+ "created_at": "2021-07-02T23:18:04.000000Z",
+ "id": 2507,
+ "name": "monster girl",
+ "slug": "monster-girl",
+ "updated_at": "2021-07-02T23:18:04.000000Z"
+ }
+ ],
+ "trash": 212,
+ "trash_rank": 244,
+ "url": "https://www.mywaifulist.moe/waifu/rachnera-arachnera-monster-musume-everyday-life-with-monster-girls",
+ "waist": "55.00",
+ "weight": "82.00"
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/getWaifuImages.json b/src/test/resources/getWaifuImages.json
new file mode 100644
index 0000000..53a4ddd
--- /dev/null
+++ b/src/test/resources/getWaifuImages.json
@@ -0,0 +1,79 @@
+{
+ "data": [
+ {
+ "id": 188451,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/5c1be30a7b7834faef76e4b4dd8a512030a0cd3437f6a5bcc75de1ac33908092.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/5c1be30a7b7834faef76e4b4dd8a512030a0cd3437f6a5bcc75de1ac33908092_thumb.jpeg"
+ },
+ {
+ "id": 178162,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/99568b61823e34d6935926d5a589181926cb83f4c4682f5b22f299f940da7503.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/99568b61823e34d6935926d5a589181926cb83f4c4682f5b22f299f940da7503_thumb.jpeg"
+ },
+ {
+ "id": 164424,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/c6d56e30ef20371df87f8967a10192140c71f03b39238c252fe6f0d49a03c1ca.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/c6d56e30ef20371df87f8967a10192140c71f03b39238c252fe6f0d49a03c1ca_thumb.jpeg"
+ },
+ {
+ "id": 164423,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/613ea8485d61fe89e52ea4a1865dff7bcf59f9d5b0d4640b78fce7778d25b9ae.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/613ea8485d61fe89e52ea4a1865dff7bcf59f9d5b0d4640b78fce7778d25b9ae_thumb.jpeg"
+ },
+ {
+ "id": 164421,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/53675eb34be747c6ff4a1004da5bedc8a8d0baf435df7966404f5c29b8bf92ae.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/53675eb34be747c6ff4a1004da5bedc8a8d0baf435df7966404f5c29b8bf92ae_thumb.jpeg"
+ },
+ {
+ "id": 164422,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/18f67d18effe733f394c643fbf9735efbfe955497d5bcddb984ba78f66718f5c.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/18f67d18effe733f394c643fbf9735efbfe955497d5bcddb984ba78f66718f5c_thumb.jpeg"
+ },
+ {
+ "id": 164419,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/de2eca1b6ffaa99134427eef444edf84e5274dc1812b014a70871e39eb380ef8.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/de2eca1b6ffaa99134427eef444edf84e5274dc1812b014a70871e39eb380ef8_thumb.jpeg"
+ },
+ {
+ "id": 164420,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/e52536c04ebf17ac73bfa45526be95b2271e23802740591c0455e1d99f1b0f09.png",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/e52536c04ebf17ac73bfa45526be95b2271e23802740591c0455e1d99f1b0f09_thumb.png"
+ },
+ {
+ "id": 164418,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/9662ce327814ceb88fd77dbfa1456dfce2b6addcf2d7bb0f9b335a35136f1d33.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/9662ce327814ceb88fd77dbfa1456dfce2b6addcf2d7bb0f9b335a35136f1d33_thumb.jpeg"
+ },
+ {
+ "id": 113519,
+ "image": "https://thicc.mywaifulist.moe/waifus/1/2543fd22eafb15ec6e8810c11ce25161f058bbb9315e05845f593bbb4f26f859.jpeg",
+ "nsfw": false,
+ "thumbnail": "https://thicc.mywaifulist.moe/waifus/1/2543fd22eafb15ec6e8810c11ce25161f058bbb9315e05845f593bbb4f26f859_thumb.jpeg"
+ }
+ ],
+ "links": {
+ "first": "https://mywaifulist.moe/api/v1/waifu/1/images?page=1",
+ "last": "https://mywaifulist.moe/api/v1/waifu/1/images?page=13",
+ "next": "https://mywaifulist.moe/api/v1/waifu/1/images?page=2",
+ "prev": null
+ },
+ "meta": {
+ "current_page": 1,
+ "from": 1,
+ "last_page": 13,
+ "path": "https://mywaifulist.moe/api/v1/waifu/1/images",
+ "per_page": 10,
+ "to": 10,
+ "total": 123
+ }
+}
\ No newline at end of file