├── core ├── src │ └── main │ │ └── java │ │ └── org │ │ └── mineskin │ │ ├── data │ │ ├── DelayInfo.java │ │ ├── SkinUrls.java │ │ ├── LimitInfo.java │ │ ├── SkinHashes.java │ │ ├── UsageInfo.java │ │ ├── NextRequest.java │ │ ├── User.java │ │ ├── ValueAndSignature.java │ │ ├── CreditsUsageInfo.java │ │ ├── Texture.java │ │ ├── RateLimitInfo.java │ │ ├── GeneratorInfo.java │ │ ├── MutableBreadcrumbed.java │ │ ├── Breadcrumbed.java │ │ ├── TextureInfo.java │ │ ├── Skin.java │ │ ├── CodeAndMessage.java │ │ ├── Visibility.java │ │ ├── Variant.java │ │ ├── JobStatus.java │ │ ├── NullJobReference.java │ │ ├── JobReference.java │ │ ├── UserInfo.java │ │ ├── Grants.java │ │ ├── JobInfo.java │ │ └── SkinInfo.java │ │ ├── response │ │ ├── ResponseWithRateLimit.java │ │ ├── UserResponse.java │ │ ├── SkinResponse.java │ │ ├── QueueResponse.java │ │ ├── GenerateResponse.java │ │ ├── ResponseConstructor.java │ │ ├── JobResponse.java │ │ ├── SkinResponseImpl.java │ │ ├── UserResponseImpl.java │ │ ├── MineSkinResponse.java │ │ ├── QueueResponseImpl.java │ │ ├── GenerateResponseImpl.java │ │ ├── JobResponseImpl.java │ │ └── AbstractMineSkinResponse.java │ │ ├── exception │ │ ├── IBreadcrumbException.java │ │ ├── MineSkinRequestException.java │ │ └── MineskinException.java │ │ ├── request │ │ ├── UrlRequestBuilder.java │ │ ├── UserRequestBuilder.java │ │ ├── UploadRequestBuilder.java │ │ ├── RequestHandlerConstructor.java │ │ ├── UrlRequestBuilderImpl.java │ │ ├── UserRequestBuilderImpl.java │ │ ├── source │ │ │ ├── InputStreamUploadSource.java │ │ │ ├── FileUploadSource.java │ │ │ ├── UploadSource.java │ │ │ └── RenderedImageUploadSource.java │ │ ├── UploadRequestBuilderImpl.java │ │ ├── backoff │ │ │ ├── RequestInterval.java │ │ │ └── ExponentialBackoff.java │ │ ├── AbstractRequestBuilder.java │ │ ├── RequestHandler.java │ │ └── GenerateRequest.java │ │ ├── options │ │ ├── IQueueOptions.java │ │ ├── IJobCheckOptions.java │ │ ├── GetQueueOptions.java │ │ ├── GenerateQueueOptions.java │ │ └── AutoGenerateQueueOptions.java │ │ ├── MiscClient.java │ │ ├── RequestExecutors.java │ │ ├── GenerateClient.java │ │ ├── MineSkinClient.java │ │ ├── SkinsClient.java │ │ ├── ImageUtil.java │ │ ├── QueueClient.java │ │ ├── GenerateOptions.java │ │ ├── QueueOptions.java │ │ ├── RequestQueue.java │ │ ├── JobChecker.java │ │ ├── JobCheckOptions.java │ │ ├── ClientBuilder.java │ │ └── MineSkinClientImpl.java ├── java-client (1).iml └── pom.xml ├── java11 ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── mineskin │ └── Java11RequestHandler.java ├── settings.xml ├── jsoup ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── mineskin │ └── JsoupRequestHandler.java ├── .github └── workflows │ ├── maven-test.yml │ └── maven-publish.yml ├── LICENSE ├── apache ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── mineskin │ └── ApacheRequestHandler.java ├── tests ├── src │ ├── test │ │ └── java │ │ │ └── test │ │ │ ├── MiscTest.java │ │ │ ├── GetTest.java │ │ │ ├── BenchmarkTest.java │ │ │ └── GenerateTest.java │ └── main │ │ └── java │ │ └── org │ │ └── mineskin │ │ └── Example.java └── pom.xml ├── .travis.yml ├── pom.xml └── README.md /core/src/main/java/org/mineskin/data/DelayInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record DelayInfo(int millis, int seconds) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/SkinUrls.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record SkinUrls(String skin, String cape) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/LimitInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record LimitInfo(int limit, int remaining) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/SkinHashes.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record SkinHashes(String skin, String cape) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/UsageInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record UsageInfo(CreditsUsageInfo credits) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/NextRequest.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record NextRequest(long absolute, long relative) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/ResponseWithRateLimit.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | public interface ResponseWithRateLimit { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/User.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public interface User { 4 | String uuid(); 5 | 6 | Grants grants(); 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/ValueAndSignature.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record ValueAndSignature(String value, String signature) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/CreditsUsageInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | @Deprecated 4 | public record CreditsUsageInfo(int used, int remaining) { 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/Texture.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | @Deprecated 4 | public record Texture(String value, String signature, String url) { 5 | } -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/RateLimitInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record RateLimitInfo(NextRequest next, DelayInfo delay, LimitInfo limit) { 4 | } 5 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/GeneratorInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record GeneratorInfo( 4 | long timestamp, 5 | long duration 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/UserResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import org.mineskin.data.User; 4 | 5 | public interface UserResponse { 6 | User getUser(); 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/MutableBreadcrumbed.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public interface MutableBreadcrumbed extends Breadcrumbed { 4 | void setBreadcrumb(String breadcrumb); 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/Breadcrumbed.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public interface Breadcrumbed { 6 | @Nullable 7 | String getBreadcrumb(); 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/TextureInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public record TextureInfo( 4 | ValueAndSignature data, 5 | SkinHashes hash, 6 | SkinUrls url 7 | ) { 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/exception/IBreadcrumbException.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.exception; 2 | 3 | import org.mineskin.data.Breadcrumbed; 4 | 5 | public interface IBreadcrumbException extends Breadcrumbed { 6 | } 7 | -------------------------------------------------------------------------------- /core/java-client (1).iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/UrlRequestBuilder.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import java.net.URL; 4 | 5 | public interface UrlRequestBuilder extends GenerateRequest { 6 | 7 | URL getUrl(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/SkinResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import org.mineskin.data.SkinInfo; 4 | 5 | public interface SkinResponse extends MineSkinResponse { 6 | SkinInfo getSkin(); 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/UserRequestBuilder.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import java.util.UUID; 4 | 5 | public interface UserRequestBuilder extends GenerateRequest { 6 | 7 | UUID getUuid(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/UploadRequestBuilder.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import org.mineskin.request.source.UploadSource; 4 | 5 | public interface UploadRequestBuilder extends GenerateRequest { 6 | 7 | UploadSource getUploadSource(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/RequestHandlerConstructor.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import com.google.gson.Gson; 4 | 5 | public interface RequestHandlerConstructor { 6 | RequestHandler construct(String baseUrl, String userAgent, String apiKey, int timeout, Gson gson); 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/Skin.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | public interface Skin { 4 | 5 | String uuid(); 6 | 7 | String name(); 8 | 9 | Variant variant(); 10 | 11 | Visibility visibility(); 12 | 13 | TextureInfo texture(); 14 | 15 | int views(); 16 | 17 | boolean duplicate(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/CodeAndMessage.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import com.google.gson.reflect.TypeToken; 4 | 5 | import java.util.List; 6 | 7 | public record CodeAndMessage(String code, String message) { 8 | 9 | public static final TypeToken> LIST_TYPE_TOKEN = new TypeToken<>() { 10 | }; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/options/IQueueOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.options; 2 | 3 | import java.util.concurrent.ScheduledExecutorService; 4 | 5 | /** 6 | * Base implementation: {@link org.mineskin.QueueOptions} 7 | */ 8 | public interface IQueueOptions { 9 | ScheduledExecutorService scheduler(); 10 | 11 | int intervalMillis(); 12 | 13 | int concurrency(); 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/QueueResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import org.mineskin.data.JobInfo; 4 | import org.mineskin.data.RateLimitInfo; 5 | import org.mineskin.data.UsageInfo; 6 | 7 | public interface QueueResponse extends MineSkinResponse { 8 | JobInfo getJob(); 9 | 10 | RateLimitInfo getRateLimit(); 11 | 12 | UsageInfo getUsage(); 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/GenerateResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import org.mineskin.data.RateLimitInfo; 4 | import org.mineskin.data.SkinInfo; 5 | import org.mineskin.data.UsageInfo; 6 | 7 | public interface GenerateResponse extends MineSkinResponse { 8 | SkinInfo getSkin(); 9 | 10 | RateLimitInfo getRateLimit(); 11 | 12 | UsageInfo getUsage(); 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/MiscClient.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.response.UserResponse; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | public interface MiscClient { 8 | /** 9 | * Get the current user 10 | * @see Get the current user 11 | */ 12 | CompletableFuture getUser(); 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/UrlRequestBuilderImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import java.net.URL; 4 | 5 | public class UrlRequestBuilderImpl extends AbstractRequestBuilder implements UrlRequestBuilder { 6 | 7 | private final URL url; 8 | 9 | UrlRequestBuilderImpl(URL url) { 10 | this.url = url; 11 | } 12 | 13 | @Override 14 | public URL getUrl() { 15 | return url; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/ResponseConstructor.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | 6 | import java.util.Map; 7 | 8 | public interface ResponseConstructor> { 9 | R construct(int status, 10 | Map headers, 11 | JsonObject rawBody, 12 | Gson gson, Class clazz); 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/RequestExecutors.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.options.IJobCheckOptions; 4 | import org.mineskin.options.IQueueOptions; 5 | 6 | import java.util.concurrent.Executor; 7 | 8 | public record RequestExecutors( 9 | Executor getExecutor, 10 | Executor generateExecutor, 11 | IQueueOptions generateQueueOptions, 12 | IQueueOptions getQueueOptions, 13 | IJobCheckOptions jobCheckOptions 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/UserRequestBuilderImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import java.util.UUID; 4 | 5 | public class UserRequestBuilderImpl extends AbstractRequestBuilder implements UserRequestBuilder { 6 | 7 | private final UUID uuid; 8 | 9 | UserRequestBuilderImpl(UUID uuid) { 10 | this.uuid = uuid; 11 | } 12 | 13 | @Override 14 | public UUID getUuid() { 15 | return uuid; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/Visibility.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public enum Visibility { 6 | @SerializedName("public") 7 | PUBLIC("public"), 8 | @SerializedName("unlisted") 9 | UNLISTED("unlisted"), 10 | @SerializedName("private") 11 | PRIVATE("private"); 12 | 13 | private final String name; 14 | 15 | Visibility(String name) { 16 | this.name = name; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/Variant.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public enum Variant { 6 | @SerializedName("") 7 | AUTO(""), 8 | @SerializedName("classic") 9 | CLASSIC("classic"), 10 | @SerializedName("slim") 11 | SLIM("slim"); 12 | 13 | private final String name; 14 | 15 | Variant(String name) { 16 | this.name = name; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/GenerateClient.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.request.GenerateRequest; 4 | import org.mineskin.response.GenerateResponse; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface GenerateClient { 9 | 10 | /** 11 | * Generate a skin 12 | * @see Generate a skin 13 | */ 14 | CompletableFuture submitAndWait(GenerateRequest request); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/source/InputStreamUploadSource.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request.source; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | public class InputStreamUploadSource implements UploadSource { 7 | 8 | private final InputStream inputStream; 9 | 10 | InputStreamUploadSource(InputStream inputStream) { 11 | this.inputStream = inputStream; 12 | } 13 | 14 | @Override 15 | public InputStream getInputStream() throws IOException { 16 | return inputStream; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/UploadRequestBuilderImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import org.mineskin.request.source.UploadSource; 4 | 5 | public class UploadRequestBuilderImpl extends AbstractRequestBuilder implements UploadRequestBuilder { 6 | 7 | private final UploadSource uploadSource; 8 | 9 | UploadRequestBuilderImpl(UploadSource uploadSource) { 10 | this.uploadSource = uploadSource; 11 | } 12 | 13 | @Override 14 | public UploadSource getUploadSource() { 15 | return uploadSource; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/backoff/RequestInterval.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request.backoff; 2 | 3 | public interface RequestInterval { 4 | /** 5 | * @param attempt attempt number, starting from 1 6 | * @return interval in milliseconds 7 | */ 8 | int getInterval(int attempt); 9 | 10 | static RequestInterval constant(int intervalMillis) { 11 | return attempt -> intervalMillis; 12 | } 13 | 14 | static ExponentialBackoff exponential() { 15 | return new ExponentialBackoff(200, 2000, 2, 3); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/source/FileUploadSource.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request.source; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | public class FileUploadSource implements UploadSource{ 9 | 10 | private final File file; 11 | 12 | FileUploadSource(File file) { 13 | this.file = file; 14 | } 15 | 16 | @Override 17 | public InputStream getInputStream() throws IOException { 18 | return new FileInputStream(file); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/JobResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import org.mineskin.MineSkinClient; 4 | import org.mineskin.data.JobInfo; 5 | import org.mineskin.data.JobReference; 6 | import org.mineskin.data.SkinInfo; 7 | 8 | import java.util.Optional; 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | public interface JobResponse extends MineSkinResponse, JobReference { 12 | JobInfo getJob(); 13 | 14 | Optional getSkin(); 15 | 16 | CompletableFuture getOrLoadSkin(MineSkinClient client); 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/options/IJobCheckOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.options; 2 | 3 | import org.mineskin.request.backoff.RequestInterval; 4 | 5 | import java.util.concurrent.ScheduledExecutorService; 6 | 7 | /** 8 | * Base implementation: {@link org.mineskin.JobCheckOptions} 9 | */ 10 | public interface IJobCheckOptions { 11 | ScheduledExecutorService scheduler(); 12 | 13 | RequestInterval interval(); 14 | 15 | @Deprecated 16 | int intervalMillis(); 17 | 18 | int initialDelayMillis(); 19 | 20 | int maxAttempts(); 21 | 22 | boolean useEta(); 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/SkinResponseImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.data.SkinInfo; 6 | 7 | import java.util.Map; 8 | 9 | public class SkinResponseImpl extends AbstractMineSkinResponse implements SkinResponse { 10 | 11 | public SkinResponseImpl(int status, Map headers, JsonObject rawBody, Gson gson, Class clazz) { 12 | super(status, headers, rawBody, gson, "skin", clazz); 13 | } 14 | 15 | @Override 16 | public SkinInfo getSkin() { 17 | return getBody(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/JobStatus.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public enum JobStatus { 6 | @SerializedName("waiting") 7 | WAITING, 8 | @SerializedName("processing") 9 | PROCESSING, 10 | @SerializedName("completed") 11 | COMPLETED, 12 | @SerializedName("failed") 13 | FAILED, 14 | @SerializedName("unknown") 15 | UNKNOWN, 16 | ; 17 | 18 | public boolean isPending() { 19 | return this == WAITING || this == PROCESSING; 20 | } 21 | 22 | public boolean isDone() { 23 | return this == COMPLETED || this == FAILED; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/MineSkinClient.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import java.util.logging.Logger; 4 | 5 | public interface MineSkinClient { 6 | 7 | static ClientBuilder builder() { 8 | return ClientBuilder.create(); 9 | } 10 | 11 | /** 12 | * Get the queue client 13 | */ 14 | QueueClient queue(); 15 | 16 | /** 17 | * Get the generate client 18 | */ 19 | GenerateClient generate(); 20 | 21 | /** 22 | * Get the skins client 23 | */ 24 | SkinsClient skins(); 25 | 26 | /** 27 | * Get the client for miscellaneous endpoints 28 | */ 29 | MiscClient misc(); 30 | 31 | Logger getLogger(); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/NullJobReference.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import java.util.Optional; 4 | 5 | public class NullJobReference implements JobReference{ 6 | 7 | private final JobInfo job; 8 | 9 | public NullJobReference(JobInfo job) { 10 | this.job = job; 11 | } 12 | 13 | @Override 14 | public JobInfo getJob() { 15 | return job; 16 | } 17 | 18 | @Override 19 | public Optional getSkin() { 20 | return Optional.empty(); 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "NullJobReference{" + 26 | "job=" + job + 27 | '}'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/JobReference.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import org.mineskin.MineSkinClient; 4 | import org.mineskin.response.SkinResponse; 5 | 6 | import java.util.Optional; 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | public interface JobReference { 10 | JobInfo getJob(); 11 | 12 | Optional getSkin(); 13 | 14 | default CompletableFuture getOrLoadSkin(MineSkinClient client) { 15 | if (this.getSkin().isPresent()) { 16 | return CompletableFuture.completedFuture(this.getSkin().get()); 17 | } else { 18 | return getJob().getSkin(client).thenApply(SkinResponse::getSkin); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/source/UploadSource.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request.source; 2 | 3 | import java.awt.image.RenderedImage; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | public interface UploadSource { 9 | 10 | static UploadSource of(InputStream inputStream) { 11 | return new InputStreamUploadSource(inputStream); 12 | } 13 | 14 | static UploadSource of(File file) { 15 | return new FileUploadSource(file); 16 | } 17 | 18 | static UploadSource of(RenderedImage renderedImage) { 19 | return new RenderedImageUploadSource(renderedImage); 20 | } 21 | 22 | InputStream getInputStream() throws IOException; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/SkinsClient.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.response.SkinResponse; 4 | 5 | import java.util.UUID; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface SkinsClient { 9 | 10 | /** 11 | * Get an existing skin by UUID (Note: not a player UUID) 12 | * @see Get a skin by UUID 13 | */ 14 | CompletableFuture get(UUID uuid); 15 | 16 | /** 17 | * Get an existing skin by UUID (Note: not a player UUID) 18 | * @see Get a skin by UUID 19 | */ 20 | CompletableFuture get(String uuid); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/ImageUtil.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | import java.util.Random; 6 | 7 | public class ImageUtil { 8 | 9 | public static BufferedImage randomImage(int width, int height) { 10 | Random random = new Random(); 11 | BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 12 | for (int x = 0; x < width; x++) { 13 | for (int y = 0; y < height; y++) { 14 | float r = random.nextFloat(); 15 | float g = random.nextFloat(); 16 | float b = random.nextFloat(); 17 | image.setRGB(x, y, new Color(r, g, b).getRGB()); 18 | } 19 | } 20 | return image; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/options/GetQueueOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.options; 2 | 3 | import org.mineskin.QueueOptions; 4 | 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.ScheduledExecutorService; 7 | 8 | public abstract class GetQueueOptions { 9 | 10 | /** 11 | * Creates a QueueOptions instance with default values for get requests (100ms interval, 5 concurrent requests). 12 | */ 13 | public static QueueOptions create(ScheduledExecutorService scheduler) { 14 | return new QueueOptions(scheduler, 100, 5); 15 | } 16 | 17 | /** 18 | * Creates a QueueOptions instance with default values for get requests (100ms interval, 5 concurrent requests). 19 | */ 20 | public static QueueOptions create() { 21 | return create(Executors.newSingleThreadScheduledExecutor()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /java11/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.mineskin 8 | java-client-parent 9 | 3.2.1-SNAPSHOT 10 | 11 | 12 | java-client-java11 13 | 3.2.1-SNAPSHOT 14 | 15 | 16 | 17 | org.mineskin 18 | java-client 19 | 3.2.1-SNAPSHOT 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/source/RenderedImageUploadSource.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request.source; 2 | 3 | import javax.imageio.ImageIO; 4 | import java.awt.image.RenderedImage; 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | public class RenderedImageUploadSource implements UploadSource{ 11 | 12 | private final RenderedImage image; 13 | 14 | public RenderedImageUploadSource(RenderedImage image) { 15 | this.image = image; 16 | } 17 | 18 | @Override 19 | public InputStream getInputStream() throws IOException { 20 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 21 | ImageIO.write(image, "png", outputStream); 22 | return new ByteArrayInputStream(outputStream.toByteArray()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/UserResponseImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.data.User; 6 | import org.mineskin.data.UserInfo; 7 | 8 | import java.util.Map; 9 | 10 | public class UserResponseImpl extends AbstractMineSkinResponse implements UserResponse { 11 | 12 | public UserResponseImpl(int status, Map headers, JsonObject rawBody, Gson gson, Class clazz) { 13 | super(status, headers, rawBody, gson, "skin", clazz); 14 | } 15 | 16 | @Override 17 | protected UserInfo parseBody(JsonObject rawBody, Gson gson, String primaryField, Class clazz) { 18 | return gson.fromJson(rawBody, clazz); 19 | } 20 | 21 | @Override 22 | public User getUser() { 23 | return getBody(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/MineSkinResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import org.mineskin.data.Breadcrumbed; 4 | import org.mineskin.data.CodeAndMessage; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | public interface MineSkinResponse extends Breadcrumbed { 10 | boolean isSuccess(); 11 | 12 | int getStatus(); 13 | 14 | List getMessages(); 15 | 16 | Optional getFirstMessage(); 17 | 18 | List getErrors(); 19 | 20 | boolean hasErrors(); 21 | 22 | Optional getFirstError(); 23 | 24 | Optional getErrorOrMessage(); 25 | 26 | List getWarnings(); 27 | 28 | Optional getFirstWarning(); 29 | 30 | String getServer(); 31 | 32 | String getBreadcrumb(); 33 | 34 | T getBody(); 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sonatype-nexus-releases 5 | ${env.CI_DEPLOY_USERNAME} 6 | ${env.CI_DEPLOY_PASSWORD} 7 | 8 | 9 | sonatype-nexus-snapshots 10 | ${env.CI_DEPLOY_USERNAME} 11 | ${env.CI_DEPLOY_PASSWORD} 12 | 13 | 14 | 15 | github 16 | ${env.GITHUB_ACTOR} 17 | ${env.GITHUB_TOKEN} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.mineskin 8 | java-client-parent 9 | 3.2.1-SNAPSHOT 10 | 11 | 12 | java-client 13 | 3.2.1-SNAPSHOT 14 | 15 | 16 | 17 | com.google.code.gson 18 | gson 19 | 2.10.1 20 | 21 | 22 | com.google.guava 23 | guava 24 | 32.1.2-jre 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /jsoup/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.mineskin 8 | java-client-parent 9 | 3.2.1-SNAPSHOT 10 | 11 | 12 | java-client-jsoup 13 | 3.2.1-SNAPSHOT 14 | 15 | 16 | 17 | org.mineskin 18 | java-client 19 | 3.2.1-SNAPSHOT 20 | 21 | 22 | org.jsoup 23 | jsoup 24 | 1.17.2 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/exception/MineSkinRequestException.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.exception; 2 | 3 | import org.mineskin.response.MineSkinResponse; 4 | 5 | public class MineSkinRequestException extends RuntimeException implements IBreadcrumbException { 6 | 7 | private final String code; 8 | private final MineSkinResponse response; 9 | 10 | public MineSkinRequestException(String code, String message, MineSkinResponse response) { 11 | super(message); 12 | this.code = code; 13 | this.response = response; 14 | } 15 | 16 | public MineSkinRequestException(String code, String message, MineSkinResponse response, Throwable cause) { 17 | super(message, cause); 18 | this.code = code; 19 | this.response = response; 20 | } 21 | 22 | public MineSkinResponse getResponse() { 23 | return response; 24 | } 25 | 26 | @Override 27 | public String getBreadcrumb() { 28 | if (response == null) return null; 29 | return response.getBreadcrumb(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/maven-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Test 5 | 6 | on: 7 | push: 8 | branches: ['*'] 9 | pull_request: 10 | branches: ['*'] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up JDK 17 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: '17' 26 | distribution: 'temurin' 27 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 28 | settings-path: ${{ github.workspace }} # location for the settings.xml file 29 | 30 | - name: Build with Maven 31 | run: mvn -B package test --file pom.xml 32 | env: 33 | MINESKIN_API_KEY: ${{ secrets.MINESKIN_API_KEY }} 34 | 35 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/UserInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class UserInfo implements User { 7 | 8 | @SerializedName("user") 9 | private final String uuid; 10 | private final JsonObject grants; 11 | 12 | private Grants grantsWrapper; 13 | 14 | public UserInfo(String uuid, JsonObject grants) { 15 | this.uuid = uuid; 16 | this.grants = grants; 17 | } 18 | 19 | @Override 20 | public String uuid() { 21 | return uuid; 22 | } 23 | 24 | public JsonObject rawGrants() { 25 | return grants; 26 | } 27 | 28 | @Override 29 | public Grants grants() { 30 | if (grantsWrapper == null) { 31 | grantsWrapper = new Grants(grants); 32 | } 33 | return grantsWrapper; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "UserInfo{" + 39 | "uuid='" + uuid + '\'' + 40 | ", grants=" + grants() + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/exception/MineskinException.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.exception; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public class MineskinException extends RuntimeException implements IBreadcrumbException { 6 | 7 | private String breadcrumb; 8 | 9 | public MineskinException() { 10 | } 11 | 12 | public MineskinException(String message) { 13 | super(message); 14 | } 15 | 16 | public MineskinException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public MineskinException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | public MineskinException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 25 | super(message, cause, enableSuppression, writableStackTrace); 26 | } 27 | 28 | public MineskinException withBreadcrumb(String breadcrumb) { 29 | this.breadcrumb = breadcrumb; 30 | return this; 31 | } 32 | 33 | @Nullable 34 | @Override 35 | public String getBreadcrumb() { 36 | return breadcrumb; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 inventivetalent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/QueueResponseImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.data.JobInfo; 6 | import org.mineskin.data.RateLimitInfo; 7 | import org.mineskin.data.UsageInfo; 8 | 9 | import java.util.Map; 10 | 11 | public class QueueResponseImpl extends AbstractMineSkinResponse implements QueueResponse { 12 | 13 | private final RateLimitInfo rateLimit; 14 | private final UsageInfo usage; 15 | 16 | public QueueResponseImpl(int status, Map headers, JsonObject rawBody, Gson gson, Class clazz) { 17 | super(status, headers, rawBody, gson, "job", clazz); 18 | this.rateLimit = gson.fromJson(rawBody.get("rateLimit"), RateLimitInfo.class); 19 | this.usage = gson.fromJson(rawBody.get("usage"), UsageInfo.class); 20 | } 21 | 22 | @Override 23 | public JobInfo getJob() { 24 | return getBody(); 25 | } 26 | 27 | @Override 28 | public RateLimitInfo getRateLimit() { 29 | return rateLimit; 30 | } 31 | 32 | @Override 33 | public UsageInfo getUsage() { 34 | return usage; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/GenerateResponseImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.data.RateLimitInfo; 6 | import org.mineskin.data.SkinInfo; 7 | import org.mineskin.data.UsageInfo; 8 | 9 | import java.util.Map; 10 | 11 | public class GenerateResponseImpl extends AbstractMineSkinResponse implements GenerateResponse { 12 | 13 | private final RateLimitInfo rateLimit; 14 | private final UsageInfo usage; 15 | 16 | public GenerateResponseImpl(int status, Map headers, JsonObject rawBody, Gson gson, Class clazz) { 17 | super(status, headers, rawBody, gson, "skin", clazz); 18 | this.rateLimit = gson.fromJson(rawBody.get("rateLimit"), RateLimitInfo.class); 19 | this.usage = gson.fromJson(rawBody.get("usage"), UsageInfo.class); 20 | } 21 | 22 | @Override 23 | public SkinInfo getSkin() { 24 | return getBody(); 25 | } 26 | 27 | @Override 28 | public RateLimitInfo getRateLimit() { 29 | return rateLimit; 30 | } 31 | 32 | @Override 33 | public UsageInfo getUsage() { 34 | return usage; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/QueueClient.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.data.JobInfo; 4 | import org.mineskin.data.JobReference; 5 | import org.mineskin.request.GenerateRequest; 6 | import org.mineskin.response.JobResponse; 7 | import org.mineskin.response.QueueResponse; 8 | 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | public interface QueueClient { 12 | 13 | /** 14 | * Submit a skin generation request 15 | * @see Queue skin generation 16 | */ 17 | CompletableFuture submit(GenerateRequest request); 18 | 19 | /** 20 | * Get the status of a job 21 | * @see Get Job Status 22 | */ 23 | CompletableFuture get(JobInfo jobInfo); 24 | 25 | /** 26 | * Get the status of a job 27 | * @see Get Job Status 28 | */ 29 | CompletableFuture get(String id); 30 | 31 | /** 32 | * Wait for a job to complete 33 | */ 34 | CompletableFuture waitForCompletion(JobInfo jobInfo); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: '17' 25 | distribution: 'temurin' 26 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 27 | settings-path: ${{ github.workspace }} # location for the settings.xml file 28 | 29 | - name: Build with Maven 30 | run: mvn -B package --file pom.xml 31 | env: 32 | MINESKIN_API_KEY: ${{ secrets.MINESKIN_API_KEY }} 33 | 34 | - name: Publish to GitHub Packages Apache Maven 35 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml 36 | env: 37 | GITHUB_TOKEN: ${{ github.token }} 38 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/Grants.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | 6 | import java.util.Optional; 7 | 8 | public class Grants { 9 | 10 | private final JsonObject raw; 11 | 12 | public Grants(JsonObject raw) { 13 | this.raw = raw; 14 | } 15 | 16 | public Optional getRaw(String key) { 17 | if (raw.has(key) && !raw.get(key).isJsonNull()) { 18 | return Optional.of(raw.get(key)); 19 | } 20 | return Optional.empty(); 21 | } 22 | 23 | public Optional getBoolean(String key) { 24 | return getRaw(key).map(JsonElement::getAsBoolean); 25 | } 26 | 27 | public Optional getInt(String key) { 28 | return getRaw(key).map(JsonElement::getAsInt); 29 | } 30 | 31 | public Optional getDouble(String key) { 32 | return getRaw(key).map(JsonElement::getAsDouble); 33 | } 34 | 35 | public Optional getString(String key) { 36 | return getRaw(key).map(JsonElement::getAsString); 37 | } 38 | 39 | public Optional perMinute() { 40 | return getInt("per_minute"); 41 | } 42 | 43 | public Optional concurrency() { 44 | return getInt("concurrency"); 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "Grants" + raw; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/AbstractRequestBuilder.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import org.mineskin.GenerateOptions; 4 | import org.mineskin.data.Variant; 5 | import org.mineskin.data.Visibility; 6 | 7 | import java.util.UUID; 8 | 9 | public abstract class AbstractRequestBuilder implements GenerateRequest { 10 | 11 | private GenerateOptions options = GenerateOptions.create(); 12 | 13 | @Override 14 | public GenerateRequest options(GenerateOptions options) { 15 | this.options = options; 16 | return this; 17 | } 18 | 19 | @Override 20 | public GenerateRequest visibility(Visibility visibility) { 21 | this.options.visibility(visibility); 22 | return this; 23 | } 24 | 25 | @Override 26 | public GenerateRequest variant(Variant variant) { 27 | this.options.variant(variant); 28 | return this; 29 | } 30 | 31 | @Override 32 | public GenerateRequest name(String name) { 33 | this.options.name(name); 34 | return this; 35 | } 36 | 37 | @Override 38 | public GenerateRequest cape(UUID cape) { 39 | this.options.cape(cape); 40 | return this; 41 | } 42 | 43 | @Override 44 | public GenerateRequest cape(String cape) { 45 | this.options.cape(cape); 46 | return this; 47 | } 48 | 49 | @Override 50 | public GenerateOptions options() { 51 | return options; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /apache/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.mineskin 8 | java-client-parent 9 | 3.2.1-SNAPSHOT 10 | 11 | 12 | java-client-apache 13 | 3.2.1-SNAPSHOT 14 | 15 | 16 | 22 17 | 22 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.mineskin 24 | java-client 25 | 3.2.1-SNAPSHOT 26 | 27 | 28 | 29 | org.apache.httpcomponents 30 | httpclient 31 | 4.5.14 32 | 33 | 34 | org.apache.httpcomponents 35 | httpmime 36 | 4.5.5 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/JobResponseImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.MineSkinClient; 6 | import org.mineskin.data.JobInfo; 7 | import org.mineskin.data.SkinInfo; 8 | 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | public class JobResponseImpl extends AbstractMineSkinResponse implements JobResponse { 14 | 15 | private SkinInfo skin; 16 | 17 | public JobResponseImpl(int status, Map headers, JsonObject rawBody, Gson gson, Class clazz) { 18 | super(status, headers, rawBody, gson, "job", clazz); 19 | this.skin = gson.fromJson(rawBody.get("skin"), SkinInfo.class); 20 | } 21 | 22 | @Override 23 | public JobInfo getJob() { 24 | return getBody(); 25 | } 26 | 27 | @Override 28 | public Optional getSkin() { 29 | return Optional.ofNullable(skin); 30 | } 31 | 32 | @Override 33 | public CompletableFuture getOrLoadSkin(MineSkinClient client) { 34 | if (this.skin != null) { 35 | this.skin.setBreadcrumb(getBreadcrumb()); 36 | return CompletableFuture.completedFuture(this.skin); 37 | } else { 38 | return getJob().getSkin(client).thenApply(skin -> { 39 | this.skin = skin.getSkin(); 40 | return this.skin; 41 | }); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /tests/src/test/java/test/MiscTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.mineskin.Java11RequestHandler; 5 | import org.mineskin.MineSkinClient; 6 | import org.mineskin.MineSkinClientImpl; 7 | import org.mineskin.data.User; 8 | import org.mineskin.response.UserResponse; 9 | 10 | import java.util.logging.ConsoleHandler; 11 | import java.util.logging.Level; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | 16 | public class MiscTest { 17 | 18 | static { 19 | MineSkinClientImpl.LOGGER.setLevel(Level.ALL); 20 | ConsoleHandler handler = new ConsoleHandler(); 21 | handler.setLevel(Level.ALL); 22 | MineSkinClientImpl.LOGGER.addHandler(handler); 23 | } 24 | 25 | private static final MineSkinClient JAVA11 = MineSkinClient.builder() 26 | .requestHandler(Java11RequestHandler::new) 27 | .userAgent("MineSkinClient-Java11/Tests") 28 | .apiKey(System.getenv("MINESKIN_API_KEY")) 29 | .build(); 30 | 31 | @Test 32 | public void getUserTest() { 33 | MineSkinClient client = JAVA11; 34 | UserResponse userResponse = client.misc().getUser().join(); 35 | System.out.println(userResponse); 36 | User user = userResponse.getUser(); 37 | System.out.println(user); 38 | int concurrency = user.grants().concurrency().orElseThrow(); 39 | assertTrue(concurrency > 5); 40 | int perMinute = user.grants().perMinute().orElseThrow(); 41 | assertTrue(perMinute > 50); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/options/GenerateQueueOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.options; 2 | 3 | import org.mineskin.QueueOptions; 4 | 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.ScheduledExecutorService; 7 | 8 | public abstract class GenerateQueueOptions { 9 | 10 | /** 11 | * Creates a QueueOptions instance with default values for generate requests (200ms interval, 1 concurrent request). 12 | */ 13 | public static QueueOptions create(ScheduledExecutorService scheduler) { 14 | return new QueueOptions(scheduler, 200, 1); 15 | } 16 | 17 | /** 18 | * Creates a QueueOptions instance with default values for generate requests (200ms interval, 1 concurrent request). 19 | */ 20 | public static QueueOptions create() { 21 | return create(Executors.newSingleThreadScheduledExecutor()); 22 | } 23 | 24 | /** 25 | * Creates a QueueOptions instance that automatically adjusts the interval and concurrency based on the user's allowance. 26 | * 27 | * @see AutoGenerateQueueOptions 28 | */ 29 | public static AutoGenerateQueueOptions createAuto(ScheduledExecutorService scheduler) { 30 | return new AutoGenerateQueueOptions(scheduler); 31 | } 32 | 33 | /** 34 | * Creates a QueueOptions instance that automatically adjusts the interval and concurrency based on the user's allowance. 35 | * 36 | * @see AutoGenerateQueueOptions 37 | */ 38 | public static AutoGenerateQueueOptions createAuto() { 39 | return createAuto(Executors.newSingleThreadScheduledExecutor()); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.response.MineSkinResponse; 6 | import org.mineskin.response.ResponseConstructor; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.Map; 11 | 12 | public abstract class RequestHandler { 13 | 14 | protected final Gson gson; 15 | protected final String baseUrl; 16 | protected final String userAgent; 17 | protected final String apiKey; 18 | 19 | public RequestHandler(String baseUrl, String userAgent, String apiKey, int timeout, Gson gson) { 20 | this.baseUrl = baseUrl; 21 | this.userAgent = userAgent; 22 | this.apiKey = apiKey; 23 | this.gson = gson; 24 | } 25 | 26 | public String getApiKey() { 27 | return apiKey; 28 | } 29 | 30 | public abstract > R getJson(String url, Class clazz, ResponseConstructor constructor) throws IOException; 31 | 32 | public abstract > R postJson(String url, JsonObject data, Class clazz, ResponseConstructor constructor) throws IOException; 33 | 34 | public abstract > R postFormDataFile(String url, 35 | String key, String filename, InputStream in, 36 | Map data, 37 | Class clazz, ResponseConstructor constructor) throws IOException; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.mineskin 8 | java-client-parent 9 | 3.2.1-SNAPSHOT 10 | 11 | 12 | java-client-tests 13 | 3.2.1-SNAPSHOT 14 | 15 | 16 | 17 | org.mineskin 18 | java-client 19 | 3.2.1-SNAPSHOT 20 | 21 | 22 | org.mineskin 23 | java-client-jsoup 24 | 3.2.1-SNAPSHOT 25 | 26 | 27 | org.mineskin 28 | java-client-apache 29 | 3.2.1-SNAPSHOT 30 | 31 | 32 | org.mineskin 33 | java-client-java11 34 | 3.2.1-SNAPSHOT 35 | test 36 | 37 | 38 | 39 | junit 40 | junit 41 | 4.13.2 42 | test 43 | 44 | 45 | org.junit.jupiter 46 | junit-jupiter-params 47 | 5.10.2 48 | test 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/GenerateRequest.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request; 2 | 3 | import org.mineskin.GenerateOptions; 4 | import org.mineskin.data.Variant; 5 | import org.mineskin.data.Visibility; 6 | import org.mineskin.request.source.UploadSource; 7 | 8 | import java.awt.image.RenderedImage; 9 | import java.io.File; 10 | import java.io.InputStream; 11 | import java.net.MalformedURLException; 12 | import java.net.URI; 13 | import java.net.URL; 14 | import java.util.UUID; 15 | 16 | public interface GenerateRequest { 17 | 18 | /// 19 | 20 | static UploadRequestBuilder upload(UploadSource uploadSource) { 21 | return new UploadRequestBuilderImpl(uploadSource); 22 | } 23 | 24 | static UploadRequestBuilder upload(InputStream inputStream) { 25 | return upload(UploadSource.of(inputStream)); 26 | } 27 | 28 | static UploadRequestBuilder upload(File file) { 29 | return upload(UploadSource.of(file)); 30 | } 31 | 32 | static UploadRequestBuilder upload(RenderedImage renderedImage) { 33 | return upload(UploadSource.of(renderedImage)); 34 | } 35 | 36 | /// 37 | 38 | static UrlRequestBuilder url(URL url) { 39 | return new UrlRequestBuilderImpl(url); 40 | } 41 | 42 | static UrlRequestBuilder url(URI uri) throws MalformedURLException { 43 | return url(uri.toURL()); 44 | } 45 | 46 | static UrlRequestBuilder url(String url) throws MalformedURLException { 47 | return url(URI.create(url)); 48 | } 49 | 50 | /// 51 | 52 | static UserRequestBuilder user(UUID uuid) { 53 | return new UserRequestBuilderImpl(uuid); 54 | } 55 | 56 | static UserRequestBuilder user(String uuid) { 57 | return user(UUID.fromString(uuid)); 58 | } 59 | 60 | /// 61 | 62 | GenerateRequest options(GenerateOptions options); 63 | 64 | GenerateRequest visibility(Visibility visibility); 65 | 66 | GenerateRequest variant(Variant variant); 67 | 68 | GenerateRequest name(String name); 69 | 70 | GenerateRequest cape(UUID cape); 71 | 72 | GenerateRequest cape(String cape); 73 | 74 | GenerateOptions options(); 75 | 76 | } -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/request/backoff/ExponentialBackoff.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.request.backoff; 2 | 3 | public final class ExponentialBackoff implements RequestInterval { 4 | 5 | private final int initialDelayMillis; 6 | private final int maxDelayMillis; 7 | private final double multiplier; 8 | private final int freeAttempts; 9 | 10 | ExponentialBackoff( 11 | int initialDelayMillis, 12 | int maxDelayMillis, 13 | double multiplier, 14 | int freeAttempts 15 | ) { 16 | this.initialDelayMillis = initialDelayMillis; 17 | this.maxDelayMillis = maxDelayMillis; 18 | this.multiplier = multiplier; 19 | this.freeAttempts = freeAttempts; 20 | } 21 | 22 | public ExponentialBackoff withInitialDelay(int initialDelayMillis) { 23 | return new ExponentialBackoff(initialDelayMillis, this.maxDelayMillis, this.multiplier, this.freeAttempts); 24 | } 25 | 26 | public ExponentialBackoff withMaxDelay(int maxDelayMillis) { 27 | return new ExponentialBackoff(this.initialDelayMillis, maxDelayMillis, this.multiplier, this.freeAttempts); 28 | } 29 | 30 | public ExponentialBackoff withMultiplier(double multiplier) { 31 | return new ExponentialBackoff(this.initialDelayMillis, this.maxDelayMillis, multiplier, this.freeAttempts); 32 | } 33 | 34 | public ExponentialBackoff withFreeAttempts(int freeAttempts) { 35 | return new ExponentialBackoff(this.initialDelayMillis, this.maxDelayMillis, this.multiplier, freeAttempts); 36 | } 37 | 38 | @Override 39 | public int getInterval(int attempt) { 40 | if (attempt <= freeAttempts) { 41 | return initialDelayMillis; 42 | } 43 | double delay = initialDelayMillis * Math.pow(multiplier, Math.max(0, attempt - freeAttempts)); 44 | return (int) Math.min(delay, maxDelayMillis); 45 | } 46 | 47 | public int initialDelayMillis() { 48 | return initialDelayMillis; 49 | } 50 | 51 | public int maxDelayMillis() { 52 | return maxDelayMillis; 53 | } 54 | 55 | public double multiplier() { 56 | return multiplier; 57 | } 58 | 59 | public int freeAttempts() { 60 | return freeAttempts; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | script: "mvn deploy --settings settings.xml" 6 | env: 7 | global: 8 | - secure: "HMwCAOyRkEFWzHqmHH2lO2dbjceAQlzB10EGKxc4YEBMj5UGbpZQe586z52Q9IAeXSSMAEziGrabpvje/lkje7Noa5opTAi5aBcuSHBbSiuujQ6PHQcIt7oc9BuUPR7lMY0b1v1qJSejJmOS85JyKeGvcWO/KGLGzippvELEGlLmuyd4X9/0bs3d7EdjyhBj3T82KlhmpDkz16fD95xdTliLJMJ9yJrc82GVXpO8nCU6DruEg+OI6/dUp6nULdgLmaSfeMNSHH1xfKNj0jbRTUzVm+CSUdsTt+uozUjhLQDtOPpMGyjbjpegKQx7KR+aoB5V0oK+0TqdOgCkc/oP2d0GthMikmmIvxgQ+s1UddJuwixNBaGd6EqRVkWCtHMVw4iZnvd9t+/RWkEkUw0xvpdQItp+0fs6QSSkcTQW4i7gOYzp105zfN6hpFa/lviKhntKmUW5Pcyjn86BketcptlXQ/hGr5/NgrCPpV8URibYM5xCRGlGcHnIx9S2sqW37XiNSFKeH/2Z6IpxFJE/YdDl3oIr2oc3FyT0pEw+6kHlbxiLhL00e6NLbSvESo7dYw2cQt3c4X1XslztLDS/JbbYDYcDkNc2eOXxBztPWO7VpVm1yJ53aGC/w4prEhzNB8J1rIYdyH6tI8tvFkYvvSFRr+23ZfUmFsLvJ/dU1bs=" 9 | - secure: "Yzseyr5E/8ZoL/UxcCWYVVKRftrX+8Gdr+8Wtomb3b+YjDnU+nOFSLSYgLHylfkxaEmH4xiBCZPvqWiYuUk7pAQ6Nx9Si3vbrnSh4jmA1hN6vP8tzYHPUEwm+FzMZO8t2icW3DmyfTrI2MTiIZwzPTIexv2NfeskU+lv25d0N0JPpUTA+blHic3s9nhbpfEUl+MU1wDdww+KG2kZFfFixesslmRtg4vB8t5L7PIbyI/iUmRAASqepckEYr87+G9Ydd5fNUJ/IYuz8+6Pvixuz5M2iFtwzwOTP3fe3r+pI9+13GCMt5kdqkvNvFAINqn8hPU7VzfAe3qFW8LqTmRQYY/GHNSQh74lr9yiZlNWyY7bCZ/Xtk3r7lLq9W+Xw+tEAzTn2hQ59j5wNtx4prsWEvci7PaI65zPQIx88HMux5dtFKUhDRVCyjFgVmjj3PtztIsd1GaY+/3yxaStWsE9G3643Wzsyj7Nz3DSr+T3tXeP+5irma3fbGlZsgHLfOkwULhqZpD/1UXv3y4vepuxyBw6BYTI2Pb9qVS01Fq5TWl/5TAhEPFZ0Z/VRTnrrzlpVxycMNulyZ40ssE2V0eOLWI9nE3rZQ4k2O32RX009c6ZMJfC0XzNnsDuvwth9YG1M0BIMe6fB/ibFrFPcOlpLSSTl/+NDjOhYU0uv146zoo=" 10 | deploy: 11 | provider: releases 12 | api_key: 13 | secure: "bzjHaHwgDsx426i4jxhHgjGPvRxgjCe97sGzeT+KJShiRZ9sOxF6ZB5eUiFOgzXAN+x0GM7qMICbU1Lx4QtNVmbkGZEFFERbGFQFf6iuI6IRk8HF5BLTUQtyfXDqtSOtAzkQksS45a486HGC33oKi0i7RuuuqjNGv5MfjKOeeisXS8FMZAVhpEG5z+MX1QNDWt1FR2vKMYS29iN2ZTX18kFp698/BnqSn4bURKGivVqannKxj0ymWsnditZXRgkKy57KcmnVU97N6gmJVf6DlBktvY/374xGxajmt+UOYcpSSy1wdgNVBgJHKwdCQ+sMmeVsKB+yzmsmNiqMU1M8De6tyFb/D3nx872kmGUCyrr6PsoEiX6pNhXN/qUxrun5EkPNbxmf/MP9MFo1qo9klFRjnO48Vp0P0r4+GuqEJeCjUMftFdDWcfFYSRmmy2nlwjXwblJiM3ciOZsb/sCTN+oKY+SWIGreG5bPgdidQRq4i0tv1dPNB/cqB/XvFKHBfFrukKH+IMhfTkJvu9YAGsiggnTlysxlmDUVQbiI/ACiEfwrKSFBJIoO2Ps5V0D9o3joMwEE5sSenMJnfYJbmQw6kBID1Iv/eBZGmHOsgK3ele6GwrOsf2G6ld6AKry5weFNtbDGY3uNe+TVbfA//IUdv2z0i1lsggR3sJ7cAXU=" 14 | file_glob: true 15 | file: 16 | - "target/java-client*.jar" 17 | skip_cleanup: true 18 | on: 19 | tags: true 20 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/JobInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import org.mineskin.MineSkinClient; 4 | import org.mineskin.exception.MineskinException; 5 | import org.mineskin.response.SkinResponse; 6 | 7 | import javax.annotation.Nullable; 8 | import java.util.Optional; 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | public class JobInfo implements MutableBreadcrumbed { 12 | 13 | private final String id; 14 | private final JobStatus status; 15 | private final long timestamp; 16 | private final long eta; 17 | private final String result; 18 | 19 | private String breadcrumb; 20 | 21 | public JobInfo(String id, JobStatus status, long timestamp, String result) { 22 | this(id, status, timestamp, 0, result); 23 | } 24 | 25 | public JobInfo(String id, JobStatus status, long timestamp, long eta, String result) { 26 | this.id = id; 27 | this.status = status; 28 | this.timestamp = timestamp; 29 | this.eta = eta; 30 | this.result = result; 31 | } 32 | 33 | public String id() { 34 | return id; 35 | } 36 | 37 | public JobStatus status() { 38 | return status; 39 | } 40 | 41 | public long timestamp() { 42 | return timestamp; 43 | } 44 | 45 | public long eta() { 46 | return eta; 47 | } 48 | 49 | public Optional result() { 50 | return Optional.ofNullable(result); 51 | } 52 | 53 | @Nullable 54 | @Override 55 | public String getBreadcrumb() { 56 | return breadcrumb; 57 | } 58 | 59 | @Override 60 | public void setBreadcrumb(String breadcrumb) { 61 | this.breadcrumb = breadcrumb; 62 | } 63 | 64 | public CompletableFuture waitForCompletion(MineSkinClient client) { 65 | return client.queue().waitForCompletion(this); 66 | } 67 | 68 | public CompletableFuture getSkin(MineSkinClient client) { 69 | if (result == null) { 70 | throw new MineskinException("Job is not completed yet").withBreadcrumb(getBreadcrumb()); 71 | } 72 | return client.skins().get(result); 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return "JobInfo{" + 78 | "id='" + id + '\'' + 79 | ", status=" + status + 80 | ", timestamp=" + timestamp + 81 | ", eta=" + eta + 82 | ", result='" + result + '\'' + 83 | ", breadcrumb='" + breadcrumb + '\'' + 84 | '}'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/data/SkinInfo.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.data; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public class SkinInfo implements Skin, MutableBreadcrumbed { 6 | 7 | private final String uuid; 8 | private final String name; 9 | private final Variant variant; 10 | private final Visibility visibility; 11 | private final TextureInfo texture; 12 | private final GeneratorInfo generator; 13 | 14 | private final int views; 15 | private final boolean duplicate; 16 | 17 | private String breadcrumb; 18 | 19 | public SkinInfo(String uuid, String name, Variant variant, Visibility visibility, TextureInfo texture, GeneratorInfo generator, int views, boolean duplicate) { 20 | this.uuid = uuid; 21 | this.name = name; 22 | this.variant = variant; 23 | this.visibility = visibility; 24 | this.texture = texture; 25 | this.generator = generator; 26 | this.views = views; 27 | this.duplicate = duplicate; 28 | } 29 | 30 | @Override 31 | public String uuid() { 32 | return uuid; 33 | } 34 | 35 | @Override 36 | public String name() { 37 | return name; 38 | } 39 | 40 | @Override 41 | public Visibility visibility() { 42 | return visibility; 43 | } 44 | 45 | @Override 46 | public Variant variant() { 47 | return variant; 48 | } 49 | 50 | @Override 51 | public TextureInfo texture() { 52 | return texture; 53 | } 54 | 55 | public GeneratorInfo generator() { 56 | return generator; 57 | } 58 | 59 | @Override 60 | public int views() { 61 | return views; 62 | } 63 | 64 | @Override 65 | public boolean duplicate() { 66 | return duplicate; 67 | } 68 | 69 | @Nullable 70 | @Override 71 | public String getBreadcrumb() { 72 | return breadcrumb; 73 | } 74 | 75 | @Override 76 | public void setBreadcrumb(String breadcrumb) { 77 | this.breadcrumb = breadcrumb; 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | return "SkinInfo{" + 83 | "uuid='" + uuid + '\'' + 84 | ", name='" + name + '\'' + 85 | ", variant=" + variant + 86 | ", visibility=" + visibility + 87 | ", texture=" + texture + 88 | ", generator=" + generator + 89 | ", views=" + views + 90 | ", duplicate=" + duplicate + 91 | ", breadcrumb='" + breadcrumb + '\'' + 92 | '}'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/GenerateOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.data.Variant; 6 | import org.mineskin.data.Visibility; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.UUID; 11 | 12 | public class GenerateOptions { 13 | 14 | private String name; 15 | private Variant variant; 16 | private Visibility visibility; 17 | private String cape; 18 | 19 | private GenerateOptions() { 20 | } 21 | 22 | public static GenerateOptions create() { 23 | return new GenerateOptions(); 24 | } 25 | 26 | /** 27 | * Set the name of the skin (optional) 28 | */ 29 | public GenerateOptions name(String name) { 30 | this.name = name; 31 | return this; 32 | } 33 | 34 | /** 35 | * Set the variant of the skin (optional, defaults to auto-detect) 36 | */ 37 | public GenerateOptions variant(Variant variant) { 38 | this.variant = variant; 39 | return this; 40 | } 41 | 42 | /** 43 | * Set the visibility of the skin (optional, defaults to public) 44 | */ 45 | public GenerateOptions visibility(Visibility visibility) { 46 | this.visibility = visibility; 47 | return this; 48 | } 49 | 50 | /** 51 | * Set the cape UUID of the skin (optional) 52 | * 53 | * @see Get a list of known capes 54 | */ 55 | public GenerateOptions cape(UUID cape) { 56 | this.cape = cape.toString(); 57 | return this; 58 | } 59 | 60 | /** 61 | * Set the cape UUID of the skin (optional) 62 | * 63 | * @see Get a list of known capes 64 | */ 65 | public GenerateOptions cape(String cape) { 66 | this.cape = cape; 67 | return this; 68 | } 69 | 70 | protected JsonObject toJson() { 71 | JsonObject json = new JsonObject(); 72 | if (!Strings.isNullOrEmpty(name)) { 73 | json.addProperty("name", name); 74 | } 75 | if (variant != null && variant != Variant.AUTO) { 76 | json.addProperty("variant", variant.getName()); 77 | } 78 | if (visibility != null) { 79 | json.addProperty("visibility", visibility.getName()); 80 | } 81 | if (cape != null) { 82 | json.addProperty("cape", cape); 83 | } 84 | return json; 85 | } 86 | 87 | protected Map toMap() { 88 | Map data = new HashMap<>(); 89 | addTo(data); 90 | return data; 91 | } 92 | 93 | protected void addTo(Map data) { 94 | if (!Strings.isNullOrEmpty(name)) { 95 | data.put("name", name); 96 | } 97 | if (variant != null && variant != Variant.AUTO) { 98 | data.put("variant", variant.getName()); 99 | } 100 | if (visibility != null) { 101 | data.put("visibility", visibility.getName()); 102 | } 103 | if (cape != null) { 104 | data.put("cape", cape); 105 | } 106 | } 107 | 108 | public String getName() { 109 | return name; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /tests/src/main/java/org/mineskin/Example.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.data.CodeAndMessage; 4 | import org.mineskin.data.JobInfo; 5 | import org.mineskin.data.Skin; 6 | import org.mineskin.data.Visibility; 7 | import org.mineskin.exception.MineSkinRequestException; 8 | import org.mineskin.request.GenerateRequest; 9 | import org.mineskin.response.MineSkinResponse; 10 | 11 | import java.io.File; 12 | import java.util.Optional; 13 | import java.util.concurrent.CompletionException; 14 | 15 | public class Example { 16 | 17 | private static final MineSkinClient CLIENT = MineSkinClient.builder() 18 | .requestHandler(JsoupRequestHandler::new) 19 | .userAgent("MyMineSkinApp/v1.0") // TODO: update this with your own user agent 20 | .apiKey("your-api-key") // TODO: update this with your own API key (https://account.mineskin.org/keys) 21 | /* 22 | // Uncomment this if you're on a paid plan with higher concurrency limits 23 | .generateQueueOptions(new QueueOptions(Executors.newSingleThreadScheduledExecutor(), 200, 5)) 24 | */ 25 | /* 26 | // Use this to automatically adjust the queue settings based on your allowance 27 | .generateQueueOptions(QueueOptions.createAutoGenerate()) 28 | */ 29 | .build(); 30 | 31 | public static void main(String[] args) { 32 | File file = new File("skin.png"); 33 | GenerateRequest request = GenerateRequest.upload(file) 34 | .name("My Skin") 35 | .visibility(Visibility.PUBLIC); 36 | // submit queue request 37 | CLIENT.queue().submit(request) 38 | .thenCompose(queueResponse -> { 39 | JobInfo job = queueResponse.getJob(); 40 | // wait for job completion 41 | return job.waitForCompletion(CLIENT); 42 | }) 43 | .thenCompose(jobResponse -> { 44 | // get skin from job or load it from the API 45 | return jobResponse.getOrLoadSkin(CLIENT); 46 | }) 47 | .thenAccept(skinInfo -> { 48 | // do stuff with the skin 49 | System.out.println(skinInfo); 50 | System.out.println(skinInfo.texture().data().value()); 51 | System.out.println(skinInfo.texture().data().signature()); 52 | }) 53 | .exceptionally(throwable -> { 54 | throwable.printStackTrace(); 55 | if (throwable instanceof CompletionException completionException) { 56 | throwable = completionException.getCause(); 57 | } 58 | 59 | if (throwable instanceof MineSkinRequestException requestException) { 60 | // get error details 61 | MineSkinResponse response = requestException.getResponse(); 62 | Optional detailsOptional = response.getErrorOrMessage(); 63 | detailsOptional.ifPresent(details -> { 64 | System.out.println(details.code() + ": " + details.message()); 65 | }); 66 | } 67 | return null; 68 | }); 69 | 70 | CLIENT.skins().get("skinuuid") 71 | .thenAccept(response -> { 72 | // get existing skin 73 | Skin skin = response.getSkin(); 74 | System.out.println(skin); 75 | }); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/QueueOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.options.AutoGenerateQueueOptions; 4 | import org.mineskin.options.GenerateQueueOptions; 5 | import org.mineskin.options.GetQueueOptions; 6 | import org.mineskin.options.IQueueOptions; 7 | 8 | import java.util.concurrent.ScheduledExecutorService; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * @param scheduler Executor service to run the queue - this should be a single-threaded scheduler 13 | * @param intervalMillis Interval in milliseconds between each request 14 | * @param concurrency Maximum number of concurrent requests 15 | */ 16 | public record QueueOptions( 17 | ScheduledExecutorService scheduler, 18 | int intervalMillis, 19 | int concurrency 20 | ) implements IQueueOptions { 21 | 22 | /** 23 | * Creates a QueueOptions instance with default values for generate requests (200ms interval, 1 concurrent request). 24 | * 25 | * @deprecated use {@link org.mineskin.options.GenerateQueueOptions#create(ScheduledExecutorService)} 26 | */ 27 | @Deprecated 28 | public static QueueOptions createGenerate(ScheduledExecutorService scheduler) { 29 | return GenerateQueueOptions.create(scheduler); 30 | } 31 | 32 | /** 33 | * Creates a QueueOptions instance with default values for generate requests (200ms interval, 1 concurrent request). 34 | * 35 | * @deprecated use {@link org.mineskin.options.GenerateQueueOptions#create()} 36 | */ 37 | @Deprecated 38 | public static QueueOptions createGenerate() { 39 | return GenerateQueueOptions.create(); 40 | } 41 | 42 | /** 43 | * Creates a QueueOptions instance that automatically adjusts the interval and concurrency based on the user's allowance. 44 | * 45 | * @see AutoGenerateQueueOptions 46 | * @deprecated use {@link GenerateQueueOptions#createAuto(ScheduledExecutorService)} 47 | */ 48 | @Deprecated 49 | public static AutoGenerateQueueOptions createAutoGenerate(ScheduledExecutorService scheduler) { 50 | return GenerateQueueOptions.createAuto(scheduler); 51 | } 52 | 53 | /** 54 | * Creates a QueueOptions instance that automatically adjusts the interval and concurrency based on the user's allowance. 55 | * 56 | * @see AutoGenerateQueueOptions 57 | * @deprecated use {@link GenerateQueueOptions#createAuto()} 58 | */ 59 | @Deprecated 60 | public static AutoGenerateQueueOptions createAutoGenerate() { 61 | return GenerateQueueOptions.createAuto(); 62 | } 63 | 64 | /** 65 | * Creates a QueueOptions instance with default values for get requests (100ms interval, 5 concurrent requests). 66 | * 67 | * @deprecated use {@link org.mineskin.options.GetQueueOptions#create(ScheduledExecutorService)} 68 | */ 69 | @Deprecated 70 | public static QueueOptions createGet(ScheduledExecutorService scheduler) { 71 | return GetQueueOptions.create(scheduler); 72 | } 73 | 74 | /** 75 | * Creates a QueueOptions instance with default values for get requests (100ms interval, 5 concurrent requests). 76 | * 77 | * @deprecated use {@link GetQueueOptions#create()} 78 | */ 79 | @Deprecated 80 | public static QueueOptions createGet() { 81 | return GetQueueOptions.create(); 82 | } 83 | 84 | public QueueOptions withInterval(int interval, TimeUnit unit) { 85 | return new QueueOptions(scheduler, (int) unit.toMillis(interval), concurrency); 86 | } 87 | 88 | public QueueOptions withConcurrency(int concurrency) { 89 | return new QueueOptions(scheduler, intervalMillis, concurrency); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/RequestQueue.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.options.IQueueOptions; 4 | 5 | import java.util.LinkedList; 6 | import java.util.Queue; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.Executor; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.function.Supplier; 13 | import java.util.logging.Level; 14 | 15 | public class RequestQueue { 16 | 17 | private final Queue>> queue = new LinkedList<>(); 18 | private final AtomicInteger running = new AtomicInteger(0); 19 | 20 | private final ScheduledExecutorService executor; 21 | private final Supplier interval; 22 | private final Supplier concurrency; 23 | 24 | private long nextRequest = 0; 25 | 26 | public RequestQueue(IQueueOptions options) { 27 | this(options.scheduler(), options::intervalMillis, options::concurrency); 28 | } 29 | 30 | public RequestQueue(ScheduledExecutorService executor, int interval, int concurrency) { 31 | this(executor, () -> interval, () -> concurrency); 32 | } 33 | 34 | public RequestQueue(ScheduledExecutorService executor, Supplier interval, Supplier concurrency) { 35 | this.executor = executor; 36 | this.interval = interval; 37 | this.concurrency = concurrency; 38 | 39 | tickAndSchedule(); 40 | } 41 | 42 | private void tickAndSchedule() { 43 | try { 44 | tick(); 45 | } catch (Throwable throwable) { 46 | MineSkinClientImpl.LOGGER.log(Level.SEVERE, "Error in request queue tick", throwable); 47 | } 48 | executor.schedule(this::tickAndSchedule, interval.get(), TimeUnit.MILLISECONDS); 49 | } 50 | 51 | private void tick() { 52 | if (System.currentTimeMillis() < nextRequest) { 53 | MineSkinClientImpl.LOGGER.log(Level.FINER, "Waiting for next request in {0}ms ({1})", new Object[]{nextRequest - System.currentTimeMillis(), hashCode()}); 54 | return; 55 | } 56 | if (running.get() >= concurrency.get()) { 57 | MineSkinClientImpl.LOGGER.log(Level.FINER, "Skipping request, already running {0} tasks", running.get()); 58 | return; 59 | } 60 | Supplier> supplier; 61 | if ((supplier = queue.poll()) != null) { 62 | MineSkinClientImpl.LOGGER.log(Level.FINER, "Processing request, running {0} tasks", running.get()); 63 | running.incrementAndGet(); 64 | supplier.get(); 65 | } 66 | } 67 | 68 | public int getInterval() { 69 | return interval.get(); 70 | } 71 | 72 | public int getConcurrency() { 73 | return concurrency.get(); 74 | } 75 | 76 | public void setNextRequest(long nextRequest) { 77 | this.nextRequest = nextRequest; 78 | } 79 | 80 | public long getNextRequest() { 81 | return nextRequest; 82 | } 83 | 84 | public int getRunning() { 85 | return running.get(); 86 | } 87 | 88 | public CompletableFuture submit(Supplier> supplier) { 89 | CompletableFuture future = new CompletableFuture<>(); 90 | queue.add(() -> supplier.get().whenComplete((result, throwable) -> { 91 | running.decrementAndGet(); 92 | if (throwable != null) { 93 | future.completeExceptionally(throwable); 94 | } else { 95 | future.complete(result); 96 | } 97 | })); 98 | return future; 99 | } 100 | 101 | public CompletableFuture submit(Supplier supplier, Executor executor) { 102 | return submit(() -> CompletableFuture.supplyAsync(supplier, executor)); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /tests/src/test/java/test/GetTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import com.google.common.collect.Lists; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.Arguments; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | import org.mineskin.*; 8 | import org.mineskin.data.CodeAndMessage; 9 | import org.mineskin.data.Visibility; 10 | import org.mineskin.exception.MineSkinRequestException; 11 | import org.mineskin.response.SkinResponse; 12 | 13 | import java.util.List; 14 | import java.util.concurrent.CompletionException; 15 | import java.util.logging.ConsoleHandler; 16 | import java.util.logging.Level; 17 | import java.util.stream.Stream; 18 | 19 | import static org.junit.jupiter.api.Assertions.*; 20 | 21 | 22 | public class GetTest { 23 | 24 | static { 25 | MineSkinClientImpl.LOGGER.setLevel(Level.ALL); 26 | ConsoleHandler handler = new ConsoleHandler(); 27 | handler.setLevel(Level.ALL); 28 | MineSkinClientImpl.LOGGER.addHandler(handler); 29 | } 30 | 31 | private static final MineSkinClient APACHE = MineSkinClient.builder() 32 | .requestHandler(ApacheRequestHandler::new) 33 | .userAgent("MineSkinClient-Apache/Tests") 34 | .apiKey(System.getenv("MINESKIN_API_KEY")) 35 | .build(); 36 | private static final MineSkinClient JSOUP = MineSkinClient.builder() 37 | .requestHandler(JsoupRequestHandler::new) 38 | .userAgent("MineSkinClient-Jsoup/Tests") 39 | .apiKey(System.getenv("MINESKIN_API_KEY")) 40 | .build(); 41 | private static final MineSkinClient JAVA11 = MineSkinClient.builder() 42 | .requestHandler(Java11RequestHandler::new) 43 | .userAgent("MineSkinClient-Java11/Tests") 44 | .apiKey(System.getenv("MINESKIN_API_KEY")) 45 | .build(); 46 | 47 | private static Stream clients() { 48 | return Stream.of( 49 | Arguments.of(APACHE), 50 | Arguments.of(JSOUP), 51 | Arguments.of(JAVA11) 52 | ); 53 | } 54 | 55 | private static Stream clientsAndSkinIds() { 56 | List clients = List.of(APACHE, JSOUP, JAVA11); 57 | List skinIds = List.of( 58 | "c1a1982831874868a37f5be375d38d5b", 59 | "9c6409112aae4c7fb4f5d026a60dcdaf", 60 | "7117a52ab80d447d887179609a4bb00c" 61 | ); 62 | List> lists = Lists.cartesianProduct(clients, skinIds); 63 | return lists.stream().map(Arguments::of); 64 | } 65 | 66 | @ParameterizedTest 67 | @MethodSource("clientsAndSkinIds") 68 | public void getUuid(List args) { 69 | MineSkinClient client = (MineSkinClient) args.get(0); 70 | String skinId = (String) args.get(1); 71 | SkinResponse res = client.skins().get(skinId).join(); 72 | System.out.println(res); 73 | assertTrue(res.isSuccess()); 74 | assertEquals(200, res.getStatus()); 75 | assertNotNull(res.getSkin()); 76 | assertEquals(skinId, res.getSkin().uuid()); 77 | assertEquals(Visibility.UNLISTED, res.getSkin().visibility()); 78 | } 79 | 80 | // 81 | @ParameterizedTest 82 | @MethodSource("clients") 83 | public void getUuidNotFound(MineSkinClient client) { 84 | CompletionException root = assertThrows(CompletionException.class, () -> client.skins().get("8cadf501765e412fbdfa1a3fa9a87711").join()); 85 | assertInstanceOf(MineSkinRequestException.class, root.getCause()); 86 | MineSkinRequestException exception = (MineSkinRequestException) root.getCause(); 87 | assertEquals("Skin not found", exception.getMessage()); 88 | assertNotNull(exception.getResponse()); 89 | assertFalse(exception.getResponse().isSuccess()); 90 | assertEquals(404, exception.getResponse().getStatus()); 91 | assertEquals("Skin not found", exception.getResponse().getFirstError().map(CodeAndMessage::message).orElse(null)); 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/options/AutoGenerateQueueOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.options; 2 | 3 | import org.mineskin.MineSkinClient; 4 | import org.mineskin.MineSkinClientImpl; 5 | import org.mineskin.data.User; 6 | import org.mineskin.response.UserResponse; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | import java.util.concurrent.atomic.AtomicLong; 14 | import java.util.logging.Level; 15 | 16 | public class AutoGenerateQueueOptions implements IQueueOptions { 17 | 18 | private static final int MIN_INTERVAL_MILLIS = 100; 19 | private static final int MAX_INTERVAL_MILLIS = 1000; 20 | private static final int MIN_CONCURRENCY = 1; 21 | private static final int MAX_CONCURRENCY = 30; 22 | 23 | private final ScheduledExecutorService scheduler; 24 | private MineSkinClient client; 25 | 26 | private final AtomicLong lastRefresh = new AtomicLong(0); 27 | 28 | private final AtomicInteger intervalMillis = new AtomicInteger(MAX_INTERVAL_MILLIS); 29 | private final AtomicInteger concurrency = new AtomicInteger(MIN_INTERVAL_MILLIS); 30 | 31 | public AutoGenerateQueueOptions( 32 | ScheduledExecutorService scheduler 33 | ) { 34 | this.scheduler = scheduler; 35 | } 36 | 37 | public AutoGenerateQueueOptions() { 38 | this(Executors.newSingleThreadScheduledExecutor()); 39 | } 40 | 41 | public void setClient(MineSkinClient client) { 42 | this.client = client; 43 | // Initial load 44 | reloadGrants().exceptionally(throwable -> { 45 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to load grants", throwable); 46 | return null; 47 | }); 48 | } 49 | 50 | @Override 51 | public ScheduledExecutorService scheduler() { 52 | return scheduler; 53 | } 54 | 55 | @Override 56 | public int intervalMillis() { 57 | reloadIfNeeded(); 58 | return intervalMillis.get(); 59 | } 60 | 61 | @Override 62 | public int concurrency() { 63 | reloadIfNeeded(); 64 | return concurrency.get(); 65 | } 66 | 67 | private void reloadIfNeeded() { 68 | long now = System.currentTimeMillis(); 69 | if (now - lastRefresh.get() > TimeUnit.MINUTES.toMillis(5)) { // 5 minutes 70 | reloadGrants().exceptionally(throwable -> { 71 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to reload grants", throwable); 72 | return null; 73 | }); 74 | } 75 | } 76 | 77 | public CompletableFuture reloadGrants() { 78 | if (client == null) { 79 | return CompletableFuture.completedFuture(null); 80 | } 81 | lastRefresh.set(System.currentTimeMillis()); 82 | 83 | return client.misc().getUser() 84 | .thenApply(UserResponse::getUser) 85 | .thenApply(User::grants) 86 | .thenAccept(grants -> { 87 | grants.concurrency().ifPresent(rawConcurrent -> { 88 | int concurrent = Math.min(Math.max(rawConcurrent, MIN_CONCURRENCY), MAX_CONCURRENCY); 89 | int previous = concurrency.getAndSet(concurrent); 90 | if (previous != rawConcurrent) { 91 | client.getLogger().log(Level.FINE, "[QueueOptions] Updated concurrency from {0} to {1}", new Object[]{previous, concurrent}); 92 | } 93 | }); 94 | grants.perMinute().ifPresent(rawPerMinute -> { 95 | int interval = Math.min(Math.max(60_000 / rawPerMinute, MIN_INTERVAL_MILLIS), MAX_INTERVAL_MILLIS); 96 | int previous = intervalMillis.getAndSet(interval); 97 | if (previous != interval) { 98 | client.getLogger().log(Level.FINE, "[QueueOptions] Updated interval from {0}ms to {1}ms (perMinute={2})", new Object[]{previous, interval, rawPerMinute}); 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.mineskin 8 | java-client-parent 9 | 3.2.1-SNAPSHOT 10 | pom 11 | 12 | core 13 | apache 14 | jsoup 15 | java11 16 | tests 17 | 18 | 19 | 20 | 17 21 | 17 22 | UTF-8 23 | 24 | 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-compiler-plugin 30 | 3.8.1 31 | 32 | 17 33 | 17 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-source-plugin 39 | 3.3.1 40 | 41 | 42 | attach-sources 43 | deploy 44 | 45 | jar-no-fork 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-javadoc-plugin 53 | 3.11.1 54 | 55 | 56 | attach-javadocs 57 | deploy 58 | 59 | jar 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-deploy-plugin 67 | 3.1.3 68 | 69 | 70 | deploy 71 | deploy 72 | 73 | deploy 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | inventive-repo 84 | https://repo.inventivetalent.org/repository/public/ 85 | 86 | 87 | github 88 | GitHub Packages 89 | https://maven.pkg.github.com/inventivetalentdev/mineskinclient 90 | 91 | 92 | 93 | 94 | inventive-repo 95 | https://repo.inventivetalent.org/repository/public/ 96 | 97 | 98 | 99 | 100 | 101 | sonatype-nexus-releases 102 | https://repo.inventivetalent.org/repository/maven-releases/ 103 | 104 | 105 | sonatype-nexus-snapshots 106 | https://repo.inventivetalent.org/repository/maven-snapshots/ 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/response/AbstractMineSkinResponse.java: -------------------------------------------------------------------------------- 1 | package org.mineskin.response; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.mineskin.data.CodeAndMessage; 6 | import org.mineskin.data.MutableBreadcrumbed; 7 | 8 | import java.util.*; 9 | 10 | public abstract class AbstractMineSkinResponse implements MineSkinResponse { 11 | 12 | private final boolean success; 13 | private final int status; 14 | 15 | private final List messages; 16 | private final List errors; 17 | private final List warnings; 18 | 19 | private final Map headers; 20 | private final String server; 21 | private final String breadcrumb; 22 | 23 | private final JsonObject rawBody; 24 | private final T body; 25 | 26 | public AbstractMineSkinResponse( 27 | int status, 28 | Map headers, 29 | JsonObject rawBody, 30 | Gson gson, 31 | String primaryField, Class clazz 32 | ) { 33 | if (rawBody.has("success")) { 34 | this.success = rawBody.get("success").getAsBoolean(); 35 | } else { 36 | this.success = status == 200; 37 | } 38 | this.status = status; 39 | this.messages = rawBody.has("messages") ? gson.fromJson(rawBody.get("messages"), CodeAndMessage.LIST_TYPE_TOKEN.getType()) : Collections.emptyList(); 40 | this.warnings = rawBody.has("warnings") ? gson.fromJson(rawBody.get("warnings"), CodeAndMessage.LIST_TYPE_TOKEN.getType()) : Collections.emptyList(); 41 | this.errors = rawBody.has("errors") ? gson.fromJson(rawBody.get("errors"), CodeAndMessage.LIST_TYPE_TOKEN.getType()) : Collections.emptyList(); 42 | 43 | this.headers = headers.entrySet().stream() 44 | .filter(e -> e.getKey().startsWith("mineskin-") || e.getKey().startsWith("x-mineskin-")) 45 | .collect(HashMap::new, (m, e) -> m.put(e.getKey().toLowerCase(), e.getValue()), HashMap::putAll); 46 | this.server = headers.get("mineskin-server"); 47 | this.breadcrumb = headers.get("mineskin-breadcrumb"); 48 | 49 | this.rawBody = rawBody; 50 | this.body = parseBody(rawBody, gson, primaryField, clazz); 51 | if (this.body instanceof MutableBreadcrumbed breadcrumbed) { 52 | breadcrumbed.setBreadcrumb(this.breadcrumb); 53 | } 54 | } 55 | 56 | protected T parseBody(JsonObject rawBody, Gson gson, String primaryField, Class clazz) { 57 | return gson.fromJson(rawBody.get(primaryField), clazz); 58 | } 59 | 60 | @Override 61 | public boolean isSuccess() { 62 | return success; 63 | } 64 | 65 | @Override 66 | public int getStatus() { 67 | return status; 68 | } 69 | 70 | @Override 71 | public List getMessages() { 72 | return messages; 73 | } 74 | 75 | @Override 76 | public Optional getFirstMessage() { 77 | return messages.stream().findFirst(); 78 | } 79 | 80 | @Override 81 | public List getErrors() { 82 | return errors; 83 | } 84 | 85 | @Override 86 | public boolean hasErrors() { 87 | return !errors.isEmpty(); 88 | } 89 | 90 | @Override 91 | public Optional getFirstError() { 92 | return errors.stream().findFirst(); 93 | } 94 | 95 | @Override 96 | public Optional getErrorOrMessage() { 97 | return getFirstError().or(this::getFirstMessage); 98 | } 99 | 100 | @Override 101 | public List getWarnings() { 102 | return warnings; 103 | } 104 | 105 | @Override 106 | public Optional getFirstWarning() { 107 | return warnings.stream().findFirst(); 108 | } 109 | 110 | @Override 111 | public String getServer() { 112 | return server; 113 | } 114 | 115 | @Override 116 | public String getBreadcrumb() { 117 | return breadcrumb; 118 | } 119 | 120 | @Override 121 | public T getBody() { 122 | return body; 123 | } 124 | 125 | @Override 126 | public String toString() { 127 | return getClass().getSimpleName() + "{" + 128 | "success=" + success + 129 | ", status=" + status + 130 | ", server='" + server + '\'' + 131 | ", breadcrumb='" + breadcrumb + '\'' + 132 | ", headers=" + headers + 133 | ", messages=" + messages + 134 | ", errors=" + errors + 135 | ", warnings=" + warnings + 136 | "}\n" + 137 | rawBody; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /jsoup/src/main/java/org/mineskin/JsoupRequestHandler.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParseException; 6 | import org.jsoup.Connection; 7 | import org.jsoup.Jsoup; 8 | import org.mineskin.data.CodeAndMessage; 9 | import org.mineskin.exception.MineSkinRequestException; 10 | import org.mineskin.exception.MineskinException; 11 | import org.mineskin.request.RequestHandler; 12 | import org.mineskin.response.MineSkinResponse; 13 | import org.mineskin.response.ResponseConstructor; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.util.Map; 18 | import java.util.logging.Level; 19 | import java.util.stream.Collectors; 20 | 21 | public class JsoupRequestHandler extends RequestHandler { 22 | 23 | private final String userAgent; 24 | private final String apiKey; 25 | private final int timeout; 26 | 27 | public JsoupRequestHandler( 28 | String baseUrl, 29 | String userAgent, String apiKey, 30 | int timeout, 31 | Gson gson) { 32 | super(baseUrl, userAgent, apiKey, timeout, gson); 33 | this.userAgent = userAgent; 34 | this.apiKey = apiKey; 35 | this.timeout = timeout; 36 | } 37 | 38 | private Connection requestBase(Connection.Method method, String url) { 39 | url = this.baseUrl + url; 40 | MineSkinClientImpl.LOGGER.log(Level.FINE, method + " " + url); 41 | Connection connection = Jsoup.connect(url) 42 | .method(method) 43 | .userAgent(userAgent) 44 | .ignoreContentType(true) 45 | .ignoreHttpErrors(true) 46 | .timeout(timeout); 47 | if (apiKey != null) { 48 | connection.header("Authorization", "Bearer " + apiKey); 49 | } 50 | return connection; 51 | } 52 | 53 | private > R wrapResponse(Connection.Response response, Class clazz, ResponseConstructor constructor) { 54 | try { 55 | JsonObject jsonBody = gson.fromJson(response.body(), JsonObject.class); 56 | R wrapped = constructor.construct( 57 | response.statusCode(), 58 | lowercaseHeaders(response.headers()), 59 | jsonBody, 60 | gson, clazz 61 | ); 62 | if (!wrapped.isSuccess()) { 63 | throw new MineSkinRequestException( 64 | wrapped.getFirstError().map(CodeAndMessage::code).orElse("request_failed"), 65 | wrapped.getFirstError().map(CodeAndMessage::message).orElse("Request Failed"), 66 | wrapped 67 | ); 68 | } 69 | return wrapped; 70 | } catch (JsonParseException e) { 71 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to parse response body: " + response.body(), e); 72 | throw new MineskinException("Failed to parse response", e); 73 | } 74 | } 75 | 76 | private Map lowercaseHeaders(Map headers) { 77 | return headers.entrySet().stream() 78 | .collect(Collectors.toMap(e -> e.getKey().toLowerCase(), Map.Entry::getValue)); 79 | } 80 | 81 | @Override 82 | public > R getJson(String url, Class clazz, ResponseConstructor constructor) throws IOException { 83 | Connection.Response response = requestBase(Connection.Method.GET, url).execute(); 84 | return wrapResponse(response, clazz, constructor); 85 | } 86 | 87 | @Override 88 | public > R postJson(String url, JsonObject data, Class clazz, ResponseConstructor constructor) throws IOException { 89 | Connection.Response response = requestBase(Connection.Method.POST, url) 90 | .requestBody(data.toString()) 91 | .header("Content-Type", "application/json") 92 | .execute(); 93 | return wrapResponse(response, clazz, constructor); 94 | } 95 | 96 | @Override 97 | public > R postFormDataFile(String url, 98 | String key, String filename, InputStream in, 99 | Map data, 100 | Class clazz, ResponseConstructor constructor) throws IOException { 101 | Connection connection = requestBase(Connection.Method.POST, url) 102 | .header("Content-Type", "multipart/form-data"); 103 | connection.data(key, filename, in); 104 | connection.data(data); 105 | Connection.Response response = connection.execute(); 106 | return wrapResponse(response, clazz, constructor); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/JobChecker.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.data.JobInfo; 4 | import org.mineskin.data.JobReference; 5 | import org.mineskin.data.JobStatus; 6 | import org.mineskin.exception.MineskinException; 7 | import org.mineskin.options.IJobCheckOptions; 8 | import org.mineskin.request.backoff.RequestInterval; 9 | 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | import java.util.logging.Level; 15 | 16 | public class JobChecker { 17 | 18 | private final MineSkinClient client; 19 | private JobInfo jobInfo; 20 | 21 | private final ScheduledExecutorService executor; 22 | private CompletableFuture future; 23 | 24 | private final AtomicInteger attempts = new AtomicInteger(0); 25 | private final int maxAttempts; 26 | private final int initialDelay; 27 | private final RequestInterval interval; 28 | private final TimeUnit timeUnit; 29 | private final boolean useEta; 30 | 31 | public JobChecker(MineSkinClient client, JobInfo jobInfo, IJobCheckOptions options) { 32 | this.client = client; 33 | this.jobInfo = jobInfo; 34 | this.executor = options.scheduler(); 35 | this.maxAttempts = options.maxAttempts(); 36 | this.initialDelay = options.initialDelayMillis(); 37 | this.interval = options.interval(); 38 | this.timeUnit = TimeUnit.MILLISECONDS; 39 | this.useEta = options.useEta(); 40 | } 41 | 42 | @Deprecated 43 | public JobChecker(MineSkinClient client, JobInfo jobInfo, ScheduledExecutorService executor, int maxAttempts, int initialDelaySeconds, int intervalSeconds) { 44 | this(client, jobInfo, executor, maxAttempts, initialDelaySeconds, intervalSeconds, TimeUnit.SECONDS); 45 | } 46 | 47 | @Deprecated 48 | public JobChecker(MineSkinClient client, JobInfo jobInfo, ScheduledExecutorService executor, int maxAttempts, int initialDelay, int interval, TimeUnit timeUnit) { 49 | this(client, jobInfo, executor, maxAttempts, initialDelay, interval, timeUnit, false); 50 | } 51 | 52 | @Deprecated 53 | public JobChecker(MineSkinClient client, JobInfo jobInfo, ScheduledExecutorService executor, int maxAttempts, int initialDelay, int interval, TimeUnit timeUnit, boolean useEta) { 54 | this.client = client; 55 | this.jobInfo = jobInfo; 56 | this.executor = executor; 57 | this.maxAttempts = maxAttempts; 58 | this.initialDelay = (int) timeUnit.toMillis(initialDelay); 59 | this.interval = RequestInterval.constant((int) timeUnit.toMillis(interval)); 60 | this.timeUnit = timeUnit; 61 | this.useEta = useEta; 62 | } 63 | 64 | /** 65 | * Starts checking the job status. Only call this once. 66 | * 67 | * @return A future that completes when the job is completed or failed, or exceptionally if an error occurs or max attempts is reached. 68 | */ 69 | public CompletableFuture check() { 70 | future = new CompletableFuture<>(); 71 | 72 | // Try to use the ETA to schedule the first check 73 | if (useEta && jobInfo.eta() > 1) { 74 | long delay = jobInfo.eta() - System.currentTimeMillis(); 75 | if (delay > 0) { 76 | client.getLogger().log(Level.FINER, "Scheduling first job check in {0}ms based on ETA", delay); 77 | executor.schedule(this::checkJob, delay, TimeUnit.MILLISECONDS); 78 | return future; 79 | } 80 | } 81 | 82 | // or just use the initial delay 83 | executor.schedule(this::checkJob, initialDelay, timeUnit); 84 | 85 | return future; 86 | } 87 | 88 | private void checkJob() { 89 | int attempt = attempts.incrementAndGet(); 90 | if (attempt > maxAttempts) { 91 | future.completeExceptionally(new MineskinException("Max attempts reached").withBreadcrumb(jobInfo.getBreadcrumb())); 92 | return; 93 | } 94 | client.queue().get(jobInfo) 95 | .thenAccept(response -> { 96 | JobInfo info = response.getBody(); 97 | if (info != null) { 98 | jobInfo = info; 99 | // client.getLogger().log(Level.FINER, "ETA {0} {1}", new Object[]{info.eta(), info.getBreadcrumb()}); 100 | } 101 | if (jobInfo.status() == JobStatus.COMPLETED || jobInfo.status() == JobStatus.FAILED) { 102 | future.complete(response); 103 | } else { 104 | executor.schedule(this::checkJob, interval.getInterval(attempt), timeUnit); 105 | } 106 | }) 107 | .exceptionally(throwable -> { 108 | future.completeExceptionally(throwable); 109 | return null; 110 | }); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MineskinClient 2 | 3 | Client for [api.mineskin.org](https://mineskin.org) 4 | 5 | Can be used to generate valid texture data from skin image files. 6 | You can also use [mineskin.org](https://mineskin.org) to directly generate skin data from images. 7 | 8 | **Important:** This is a client for [MineSkin V2](https://docs.mineskin.org/docs/guides/migrating-to-v2/) - for V1 see the [2.x branch](https://github.com/InventivetalentDev/MineskinClient/tree/2.x) and use versions < 3.0.0. 9 | 10 | The API requires official Minecraft accounts to upload the skin textures. 11 | If you own a Minecraft account you don't actively use and want to contibute to the API's speed, 12 | please [add your account here](https://account.mineskin.org)! 13 | 14 | ```java 15 | public class Example { 16 | 17 | private static final MineSkinClient CLIENT = MineSkinClient.builder() 18 | .requestHandler(JsoupRequestHandler::new) 19 | .userAgent("MyMineSkinApp/v1.0") // TODO: update this with your own user agent 20 | .apiKey("your-api-key") // TODO: update this with your own API key (https://account.mineskin.org/keys) 21 | /* 22 | // Uncomment this if you're on a paid plan with higher concurrency limits 23 | .generateQueueOptions(new QueueOptions(Executors.newSingleThreadScheduledExecutor(), 200, 5)) 24 | */ 25 | /* 26 | // Use this to automatically adjust the queue settings based on your allowance 27 | .generateQueueOptions(QueueOptions.createAutoGenerate()) 28 | */ 29 | .build(); 30 | 31 | public static void main(String[] args) { 32 | File file = new File("skin.png"); 33 | GenerateRequest request = GenerateRequest.upload(file) 34 | .name("My Skin") 35 | .visibility(Visibility.PUBLIC); 36 | // submit queue request 37 | CLIENT.queue().submit(request) 38 | .thenCompose(queueResponse -> { 39 | JobInfo job = queueResponse.getJob(); 40 | // wait for job completion 41 | return job.waitForCompletion(CLIENT); 42 | }) 43 | .thenCompose(jobResponse -> { 44 | // get skin from job or load it from the API 45 | return jobResponse.getOrLoadSkin(CLIENT); 46 | }) 47 | .thenAccept(skinInfo -> { 48 | // do stuff with the skin 49 | System.out.println(skinInfo); 50 | System.out.println(skinInfo.texture().data().value()); 51 | System.out.println(skinInfo.texture().data().signature()); 52 | }) 53 | .exceptionally(throwable -> { 54 | throwable.printStackTrace(); 55 | if (throwable instanceof CompletionException completionException) { 56 | throwable = completionException.getCause(); 57 | } 58 | 59 | if (throwable instanceof MineSkinRequestException requestException) { 60 | // get error details 61 | MineSkinResponse response = requestException.getResponse(); 62 | Optional detailsOptional = response.getErrorOrMessage(); 63 | detailsOptional.ifPresent(details -> { 64 | System.out.println(details.code() + ": " + details.message()); 65 | }); 66 | } 67 | return null; 68 | }); 69 | 70 | CLIENT.skins().get("skinuuid") 71 | .thenAccept(response -> { 72 | // get existing skin 73 | Skin skin = response.getSkin(); 74 | System.out.println(skin); 75 | }); 76 | } 77 | 78 | } 79 | ``` 80 | 81 | 82 | ```xml 83 | 84 | 85 | 86 | org.mineskin 87 | java-client 88 | 3.2.1-SNAPSHOT 89 | 90 | 91 | org.mineskin 92 | java-client-jsoup 93 | 3.2.1-SNAPSHOT 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ``` 109 | ```xml 110 | 111 | 112 | inventive-repo 113 | https://repo.inventivetalent.org/repository/public/ 114 | 115 | 116 | ``` 117 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/JobCheckOptions.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import org.mineskin.options.IJobCheckOptions; 4 | import org.mineskin.request.backoff.RequestInterval; 5 | 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.ScheduledExecutorService; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Example: 12 | *
 13 |  *     JobCheckOptions.create()
 14 |  *           .withUseEta()
 15 |  *           .withInterval(RequestInterval.exponential())
 16 |  *           .withMaxAttempts(50)
 17 |  * 
18 | */ 19 | public final class JobCheckOptions implements IJobCheckOptions { 20 | 21 | private final ScheduledExecutorService scheduler; 22 | private final RequestInterval interval; 23 | private final int initialDelayMillis; 24 | private final int maxAttempts; 25 | private final boolean useEta; 26 | 27 | /** 28 | * @param scheduler Executor service to run the job checks - this should be a single-threaded scheduler 29 | * @param interval Interval strategy between each request, see {@link RequestInterval} 30 | * @param initialDelayMillis Initial delay in milliseconds before the first job check, default is 2000 31 | * @param maxAttempts Maximum number of attempts to check the job status, default is 10 32 | * @param useEta Whether to use the estimated completion time provided by the server to schedule the first check, default is false 33 | */ 34 | @Deprecated 35 | public JobCheckOptions( 36 | ScheduledExecutorService scheduler, 37 | RequestInterval interval, 38 | int initialDelayMillis, 39 | int maxAttempts, 40 | boolean useEta 41 | ) { 42 | this.scheduler = scheduler; 43 | this.interval = interval; 44 | this.initialDelayMillis = initialDelayMillis; 45 | this.maxAttempts = maxAttempts; 46 | this.useEta = useEta; 47 | } 48 | 49 | /** 50 | * @param scheduler Executor service to run the job checks - this should be a single-threaded scheduler 51 | * @param intervalMillis Interval in milliseconds between each job check, default is 1000 52 | * @param initialDelayMillis Initial delay in milliseconds before the first job check, default is 2000 53 | * @param maxAttempts Maximum number of attempts to check the job status, default is 10 54 | * @param useEta Whether to use the estimated completion time provided by the server to schedule the first check, default is false 55 | */ 56 | @Deprecated 57 | public JobCheckOptions( 58 | ScheduledExecutorService scheduler, 59 | int intervalMillis, 60 | int initialDelayMillis, 61 | int maxAttempts, 62 | boolean useEta 63 | ) { 64 | this.scheduler = scheduler; 65 | this.interval = RequestInterval.constant(intervalMillis); 66 | this.initialDelayMillis = initialDelayMillis; 67 | this.maxAttempts = maxAttempts; 68 | this.useEta = useEta; 69 | } 70 | 71 | @Deprecated 72 | public JobCheckOptions( 73 | ScheduledExecutorService scheduler, 74 | int intervalMillis, 75 | int initialDelayMillis, 76 | int maxAttempts 77 | ) { 78 | this(scheduler, intervalMillis, initialDelayMillis, maxAttempts, false); 79 | } 80 | 81 | /** 82 | * Creates a JobCheckOptions instance with default values. 83 | */ 84 | public static JobCheckOptions create(ScheduledExecutorService scheduler) { 85 | return new JobCheckOptions( 86 | scheduler, 87 | 1000, 88 | 2000, 89 | 10, 90 | false 91 | ); 92 | } 93 | 94 | /** 95 | * Creates a JobCheckOptions instance with default values. 96 | */ 97 | public static JobCheckOptions create() { 98 | return create(Executors.newSingleThreadScheduledExecutor()); 99 | } 100 | 101 | public JobCheckOptions withInterval(RequestInterval interval) { 102 | return new JobCheckOptions(scheduler, interval, initialDelayMillis, maxAttempts, useEta); 103 | } 104 | 105 | public JobCheckOptions withInitialDelay(int initialDelayMillis) { 106 | return new JobCheckOptions(scheduler, interval, initialDelayMillis, maxAttempts, useEta); 107 | } 108 | 109 | public JobCheckOptions withInitialDelay(int initialDelay, TimeUnit unit) { 110 | return new JobCheckOptions(scheduler, interval, (int) unit.toMillis(initialDelay), maxAttempts, useEta); 111 | } 112 | 113 | public JobCheckOptions withMaxAttempts(int maxAttempts) { 114 | return new JobCheckOptions(scheduler, interval, initialDelayMillis, maxAttempts, useEta); 115 | } 116 | 117 | /** 118 | * Sets the option to use the estimated completion time provided by the server to schedule the first check. 119 | */ 120 | public JobCheckOptions withUseEta() { 121 | return new JobCheckOptions(scheduler, interval, initialDelayMillis, maxAttempts, true); 122 | } 123 | 124 | @Override 125 | public ScheduledExecutorService scheduler() { 126 | return scheduler; 127 | } 128 | 129 | @Override 130 | public RequestInterval interval() { 131 | return interval; 132 | } 133 | 134 | @Deprecated 135 | @Override 136 | public int intervalMillis() { 137 | return interval.getInterval(1); 138 | } 139 | 140 | @Override 141 | public int initialDelayMillis() { 142 | return initialDelayMillis; 143 | } 144 | 145 | @Override 146 | public int maxAttempts() { 147 | return maxAttempts; 148 | } 149 | 150 | @Override 151 | public boolean useEta() { 152 | return useEta; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /apache/src/main/java/org/mineskin/ApacheRequestHandler.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParseException; 6 | import org.apache.http.Header; 7 | import org.apache.http.HttpEntity; 8 | import org.apache.http.HttpResponse; 9 | import org.apache.http.client.HttpClient; 10 | import org.apache.http.client.config.RequestConfig; 11 | import org.apache.http.client.methods.HttpGet; 12 | import org.apache.http.client.methods.HttpPost; 13 | import org.apache.http.entity.ContentType; 14 | import org.apache.http.entity.StringEntity; 15 | import org.apache.http.entity.mime.MultipartEntityBuilder; 16 | import org.apache.http.impl.client.HttpClientBuilder; 17 | import org.apache.http.message.BasicHeader; 18 | import org.mineskin.data.CodeAndMessage; 19 | import org.mineskin.exception.MineSkinRequestException; 20 | import org.mineskin.exception.MineskinException; 21 | import org.mineskin.request.RequestHandler; 22 | import org.mineskin.response.MineSkinResponse; 23 | import org.mineskin.response.ResponseConstructor; 24 | 25 | import java.io.BufferedReader; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.io.InputStreamReader; 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.logging.Level; 33 | import java.util.stream.Collectors; 34 | import java.util.stream.Stream; 35 | 36 | public class ApacheRequestHandler extends RequestHandler { 37 | 38 | private final Gson gson; 39 | 40 | private final HttpClient httpClient; 41 | 42 | public ApacheRequestHandler( 43 | String baseUrl, 44 | String userAgent, String apiKey, 45 | int timeout, 46 | Gson gson) { 47 | super(baseUrl, userAgent, apiKey, timeout, gson); 48 | this.gson = gson; 49 | 50 | List
defaultHeaders = new ArrayList<>(); 51 | if (apiKey != null) { 52 | defaultHeaders.add(new BasicHeader("Authorization", "Bearer " + apiKey)); 53 | defaultHeaders.add(new BasicHeader("Accept", "application/json")); 54 | } 55 | this.httpClient = HttpClientBuilder.create() 56 | .setDefaultRequestConfig(RequestConfig.copy(RequestConfig.DEFAULT) 57 | .setSocketTimeout(timeout) 58 | .setConnectTimeout(timeout) 59 | .setConnectionRequestTimeout(timeout) 60 | .build()) 61 | .setUserAgent(userAgent) 62 | .setDefaultHeaders(defaultHeaders) 63 | .build(); 64 | } 65 | 66 | private > R wrapResponse(HttpResponse response, Class clazz, ResponseConstructor constructor) throws IOException { 67 | String rawBody = null; 68 | try { 69 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) { 70 | rawBody = reader.lines().collect(Collectors.joining("\n")); 71 | } 72 | 73 | JsonObject jsonBody = gson.fromJson(rawBody, JsonObject.class); 74 | R wrapped = constructor.construct( 75 | response.getStatusLine().getStatusCode(), 76 | lowercaseHeaders(response.getAllHeaders()), 77 | jsonBody, 78 | gson, clazz 79 | ); 80 | if (!wrapped.isSuccess()) { 81 | throw new MineSkinRequestException( 82 | wrapped.getFirstError().map(CodeAndMessage::code).orElse("request_failed"), 83 | wrapped.getFirstError().map(CodeAndMessage::message).orElse("Request Failed"), 84 | wrapped 85 | ); 86 | } 87 | return wrapped; 88 | } catch (JsonParseException e) { 89 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to parse response body: " + rawBody, e); 90 | throw new MineskinException("Failed to parse response", e); 91 | } 92 | } 93 | 94 | private Map lowercaseHeaders(Header[] headers) { 95 | return Stream.of(headers) 96 | .collect(Collectors.toMap( 97 | header -> header.getName().toLowerCase(), 98 | Header::getValue 99 | )); 100 | } 101 | 102 | @Override 103 | public > R getJson(String url, Class clazz, ResponseConstructor constructor) throws IOException { 104 | url = this.baseUrl + url; 105 | MineSkinClientImpl.LOGGER.fine("GET " + url); 106 | HttpResponse response = this.httpClient.execute(new HttpGet(url)); 107 | return wrapResponse(response, clazz, constructor); 108 | } 109 | 110 | @Override 111 | public > R postJson(String url, JsonObject data, Class clazz, ResponseConstructor constructor) throws IOException { 112 | url = this.baseUrl + url; 113 | MineSkinClientImpl.LOGGER.fine("POST " + url); 114 | HttpPost post = new HttpPost(url); 115 | post.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType()); 116 | StringEntity entity = new StringEntity(gson.toJson(data), ContentType.APPLICATION_JSON); 117 | post.setEntity(entity); 118 | HttpResponse response = this.httpClient.execute(post); 119 | return wrapResponse(response, clazz, constructor); 120 | } 121 | 122 | @Override 123 | public > R postFormDataFile(String url, String key, String filename, InputStream in, Map data, Class clazz, ResponseConstructor constructor) throws IOException { 124 | url = this.baseUrl + url; 125 | MineSkinClientImpl.LOGGER.fine("POST " + url); 126 | HttpPost post = new HttpPost(url); 127 | MultipartEntityBuilder multipart = MultipartEntityBuilder.create() 128 | .setBoundary("mineskin-" + System.currentTimeMillis()) 129 | .addBinaryBody(key, in, ContentType.IMAGE_PNG, filename); 130 | for (Map.Entry entry : data.entrySet()) { 131 | multipart.addTextBody(entry.getKey(), entry.getValue()); 132 | } 133 | HttpEntity entity = multipart.build(); 134 | post.setEntity(entity); 135 | HttpResponse response = this.httpClient.execute(post); 136 | return wrapResponse(response, clazz, constructor); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /tests/src/test/java/test/BenchmarkTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.Ignore; 4 | import org.junit.Test; 5 | import org.mineskin.*; 6 | import org.mineskin.data.Breadcrumbed; 7 | import org.mineskin.data.Visibility; 8 | import org.mineskin.exception.MineSkinRequestException; 9 | import org.mineskin.request.GenerateRequest; 10 | import org.mineskin.request.backoff.RequestInterval; 11 | import org.mineskin.response.QueueResponse; 12 | 13 | import java.awt.image.BufferedImage; 14 | import java.text.SimpleDateFormat; 15 | import java.util.ArrayList; 16 | import java.util.Date; 17 | import java.util.List; 18 | import java.util.concurrent.*; 19 | import java.util.concurrent.atomic.AtomicInteger; 20 | import java.util.logging.ConsoleHandler; 21 | import java.util.logging.Level; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | 25 | @Ignore 26 | public class BenchmarkTest { 27 | 28 | static { 29 | // set logger to log milliseconds 30 | System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT.%1$tL %4$s %2$s: %5$s%6$s%n"); 31 | 32 | MineSkinClientImpl.LOGGER.setLevel(Level.ALL); 33 | ConsoleHandler handler = new ConsoleHandler(); 34 | handler.setLevel(Level.ALL); 35 | MineSkinClientImpl.LOGGER.addHandler(handler); 36 | } 37 | 38 | private static int GENERATE_INTERVAL_MS = 100; 39 | private static int GENERATE_CONCURRENCY = 20; 40 | 41 | private static int GENERATE_AMOUNT = 200; 42 | 43 | private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(); 44 | 45 | private static final MineSkinClient JAVA11 = MineSkinClient.builder() 46 | .requestHandler(Java11RequestHandler::new) 47 | .userAgent("MineSkinClient/Benchmark") 48 | .apiKey(System.getenv("MINESKIN_API_KEY")) 49 | .generateExecutor(EXECUTOR) 50 | // .generateQueueOptions(new QueueOptions( 51 | // Executors.newSingleThreadScheduledExecutor(), 52 | // GENERATE_INTERVAL_MS, GENERATE_CONCURRENCY 53 | // )) 54 | .generateQueueOptions(QueueOptions.createAutoGenerate()) 55 | .jobCheckOptions(JobCheckOptions.create().withUseEta().withInterval(RequestInterval.exponential()).withMaxAttempts(50)) 56 | .build(); 57 | 58 | private final AtomicInteger per10s = new AtomicInteger(); 59 | private final AtomicInteger perMinute = new AtomicInteger(); 60 | private final AtomicInteger total = new AtomicInteger(); 61 | private long lastLog10s = System.currentTimeMillis(); 62 | private long lastLog = System.currentTimeMillis(); 63 | 64 | @Test 65 | public void benchmark() throws InterruptedException { 66 | 67 | log("Starting benchmark with " + GENERATE_AMOUNT + " skins, interval " + GENERATE_INTERVAL_MS + "ms, concurrency " + GENERATE_CONCURRENCY); 68 | 69 | MineSkinClient client = JAVA11; 70 | int count = GENERATE_AMOUNT; 71 | Thread.sleep(1000); 72 | 73 | CompletableFuture.runAsync(() -> { 74 | while (total.get() < count) { 75 | try { 76 | Thread.sleep(1000); 77 | } catch (InterruptedException e) { 78 | throw new RuntimeException(e); 79 | } 80 | long now = System.currentTimeMillis(); 81 | if (now - lastLog10s >= 10000) { 82 | int per10s = this.per10s.get(); 83 | log("Last 10s: " + per10s + " (" + (per10s / 10.0) + "/s)"); 84 | this.per10s.set(0); 85 | lastLog10s = now; 86 | } 87 | if (now - lastLog >= 60000) { 88 | int perMinute = this.perMinute.get(); 89 | log("Last minute: " + perMinute + " (" + (perMinute / 60.0) + "/s)"); 90 | this.perMinute.set(0); 91 | lastLog = now; 92 | } 93 | } 94 | }); 95 | 96 | long start = System.currentTimeMillis(); 97 | List> futures = new ArrayList<>(); 98 | for (int i = 0; i < count; i++) { 99 | int finalI = i; 100 | futures.add(CompletableFuture.runAsync(() -> { 101 | try { 102 | generateSkin(client, finalI); 103 | } catch (InterruptedException e) { 104 | System.out.println(e); 105 | } 106 | })); 107 | } 108 | 109 | CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); 110 | 111 | log("Generated " + count + " in " + (System.currentTimeMillis() - start) + "ms"); 112 | 113 | assertEquals(count, total.get()); 114 | 115 | Thread.sleep(1000); 116 | } 117 | 118 | private void generateSkin(MineSkinClient client, int index) throws InterruptedException { 119 | long jobStart = System.currentTimeMillis(); 120 | try { 121 | Thread.sleep(10); 122 | String name = "mskjva-bnch-" + index + "-" + ThreadLocalRandom.current().nextInt(1000); 123 | BufferedImage image = ImageUtil.randomImage(64, ThreadLocalRandom.current().nextBoolean() ? 64 : 32); 124 | GenerateRequest request = GenerateRequest.upload(image) 125 | .visibility(Visibility.UNLISTED) 126 | .name(name); 127 | QueueResponse res = client.queue().submit(request).join(); 128 | log("[" + index + "] " + res.getBreadcrumb() + " Queue submit took " + (System.currentTimeMillis() - jobStart) + "ms - " + res.getRateLimit().next()); 129 | log(res); 130 | 131 | client.queue() 132 | .waitForCompletion(res.getJob()) 133 | .thenCompose(jobReference -> { 134 | log("[" + index + "] " + res.getBreadcrumb() + " Job took " + (System.currentTimeMillis() - jobStart) + "ms"); 135 | return jobReference.getOrLoadSkin(client); 136 | }) 137 | .thenAccept(skinInfo -> { 138 | log("[" + index + "] " + res.getBreadcrumb() + " Got skin after " + (System.currentTimeMillis() - jobStart) + "ms"); 139 | log(skinInfo); 140 | per10s.incrementAndGet(); 141 | perMinute.incrementAndGet(); 142 | total.incrementAndGet(); 143 | }) 144 | .exceptionally(throwable -> { 145 | if (throwable instanceof CompletionException e && e.getCause() instanceof Breadcrumbed breadcrumb) { 146 | log(breadcrumb.getBreadcrumb() + " (exception 1)"); 147 | } 148 | if (throwable instanceof CompletionException e && e.getCause() instanceof MineSkinRequestException req) { 149 | log(req.getResponse()); 150 | } else { 151 | log(throwable); 152 | } 153 | return null; 154 | }) 155 | .join(); 156 | } catch (CompletionException | InterruptedException e) { 157 | if (e.getCause() instanceof Breadcrumbed breadcrumb) { 158 | log(breadcrumb.getBreadcrumb() + " (exception 2)"); 159 | } 160 | if (e.getCause() instanceof MineSkinRequestException req) { 161 | log(req.getResponse()); 162 | } 163 | throw e; 164 | } 165 | } 166 | 167 | static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); 168 | 169 | void log(Object message) { 170 | Date date = new Date(); 171 | System.out.println(String.format("[%s] %s", DATE_FORMAT.format(date), message)); 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /java11/src/main/java/org/mineskin/Java11RequestHandler.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParseException; 6 | import org.mineskin.data.CodeAndMessage; 7 | import org.mineskin.exception.MineSkinRequestException; 8 | import org.mineskin.exception.MineskinException; 9 | import org.mineskin.request.RequestHandler; 10 | import org.mineskin.response.MineSkinResponse; 11 | import org.mineskin.response.ResponseConstructor; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.net.URI; 16 | import java.net.http.HttpClient; 17 | import java.net.http.HttpRequest; 18 | import java.net.http.HttpResponse; 19 | import java.net.http.HttpRequest.BodyPublishers; 20 | import java.net.http.HttpResponse.BodyHandlers; 21 | import java.util.Map; 22 | import java.util.logging.Level; 23 | import java.util.stream.Collectors; 24 | 25 | public class Java11RequestHandler extends RequestHandler { 26 | 27 | private final Gson gson; 28 | private final HttpClient httpClient; 29 | 30 | public Java11RequestHandler(String baseUrl, String userAgent, String apiKey, int timeout, Gson gson) { 31 | super(baseUrl, userAgent, apiKey, timeout, gson); 32 | this.gson = gson; 33 | 34 | HttpClient.Builder clientBuilder = HttpClient.newBuilder() 35 | .connectTimeout(java.time.Duration.ofMillis(timeout)); 36 | 37 | if (userAgent != null) { 38 | clientBuilder.followRedirects(HttpClient.Redirect.NORMAL); 39 | } 40 | this.httpClient = clientBuilder.build(); 41 | } 42 | 43 | private > R wrapResponse(HttpResponse response, Class clazz, ResponseConstructor constructor) throws IOException { 44 | String rawBody = response.body(); 45 | try { 46 | JsonObject jsonBody = gson.fromJson(rawBody, JsonObject.class); 47 | R wrapped = constructor.construct( 48 | response.statusCode(), 49 | lowercaseHeaders(response.headers().map()), 50 | jsonBody, 51 | gson, clazz 52 | ); 53 | if (!wrapped.isSuccess()) { 54 | throw new MineSkinRequestException( 55 | wrapped.getFirstError().map(CodeAndMessage::code).orElse("request_failed"), 56 | wrapped.getFirstError().map(CodeAndMessage::message).orElse("Request Failed"), 57 | wrapped 58 | ); 59 | } 60 | return wrapped; 61 | } catch (JsonParseException e) { 62 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to parse response body: " + rawBody, e); 63 | throw new MineskinException("Failed to parse response", e); 64 | } 65 | } 66 | 67 | private Map lowercaseHeaders(Map> headers) { 68 | return headers.entrySet().stream() 69 | .collect(Collectors.toMap( 70 | entry -> entry.getKey().toLowerCase(), 71 | entry -> String.join(", ", entry.getValue()) 72 | )); 73 | } 74 | 75 | public > R getJson(String url, Class clazz, ResponseConstructor constructor) throws IOException { 76 | url = this.baseUrl + url; 77 | MineSkinClientImpl.LOGGER.fine("GET " + url); 78 | 79 | HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() 80 | .uri(URI.create(url)) 81 | .GET() 82 | .header("User-Agent", this.userAgent); 83 | HttpRequest request; 84 | if (apiKey != null) { 85 | request = requestBuilder 86 | .header("Authorization", "Bearer " + apiKey) 87 | .header("Accept", "application/json").build(); 88 | } else { 89 | request = requestBuilder.build(); 90 | } 91 | HttpResponse response; 92 | try { 93 | response = this.httpClient.send(request, BodyHandlers.ofString()); 94 | } catch (InterruptedException e) { 95 | throw new RuntimeException(e); 96 | } 97 | return wrapResponse(response, clazz, constructor); 98 | } 99 | 100 | public > R postJson(String url, JsonObject data, Class clazz, ResponseConstructor constructor) throws IOException { 101 | url = this.baseUrl + url; 102 | MineSkinClientImpl.LOGGER.fine("POST " + url); 103 | 104 | HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() 105 | .uri(URI.create(url)) 106 | .POST(BodyPublishers.ofString(gson.toJson(data))) 107 | .header("Content-Type", "application/json") 108 | .header("User-Agent", this.userAgent); 109 | HttpRequest request; 110 | if (apiKey != null) { 111 | request = requestBuilder 112 | .header("Authorization", "Bearer " + apiKey) 113 | .header("Accept", "application/json").build(); 114 | } else { 115 | request = requestBuilder.build(); 116 | } 117 | 118 | HttpResponse response; 119 | try { 120 | response = this.httpClient.send(request, BodyHandlers.ofString()); 121 | } catch (InterruptedException e) { 122 | throw new RuntimeException(e); 123 | } 124 | return wrapResponse(response, clazz, constructor); 125 | } 126 | 127 | public > R postFormDataFile(String url, String key, String filename, InputStream in, Map data, Class clazz, ResponseConstructor constructor) throws IOException { 128 | url = this.baseUrl + url; 129 | MineSkinClientImpl.LOGGER.fine("POST " + url); 130 | 131 | String boundary = "mineskin-" + System.currentTimeMillis(); 132 | StringBuilder bodyBuilder = new StringBuilder(); 133 | 134 | // add form fields 135 | for (Map.Entry entry : data.entrySet()) { 136 | bodyBuilder.append("--").append(boundary).append("\r\n") 137 | .append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"\r\n\r\n") 138 | .append(entry.getValue()).append("\r\n"); 139 | } 140 | 141 | // add file 142 | byte[] fileContent = in.readAllBytes(); 143 | bodyBuilder.append("--").append(boundary).append("\r\n") 144 | .append("Content-Disposition: form-data; name=\"").append(key) 145 | .append("\"; filename=\"").append(filename).append("\"\r\n") 146 | .append("Content-Type: image/png\r\n\r\n"); 147 | byte[] bodyStart = bodyBuilder.toString().getBytes(); 148 | byte[] boundaryEnd = ("\r\n--" + boundary + "--\r\n").getBytes(); 149 | byte[] bodyString = new byte[bodyStart.length + fileContent.length + boundaryEnd.length]; 150 | System.arraycopy(bodyStart, 0, bodyString, 0, bodyStart.length); 151 | System.arraycopy(fileContent, 0, bodyString, bodyStart.length, fileContent.length); 152 | System.arraycopy(boundaryEnd, 0, bodyString, bodyStart.length + fileContent.length, boundaryEnd.length); 153 | 154 | HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() 155 | .uri(URI.create(url)) 156 | .POST(HttpRequest.BodyPublishers.ofByteArray(bodyString)) 157 | .header("Content-Type", "multipart/form-data; boundary=" + boundary) 158 | .header("User-Agent", this.userAgent); 159 | HttpRequest request; 160 | if (apiKey != null) { 161 | request = requestBuilder 162 | .header("Authorization", "Bearer " + apiKey) 163 | .header("Accept", "application/json").build(); 164 | } else { 165 | request = requestBuilder.build(); 166 | } 167 | 168 | HttpResponse response; 169 | try { 170 | response = this.httpClient.send(request, BodyHandlers.ofString()); 171 | } catch (InterruptedException e) { 172 | throw new RuntimeException(e); 173 | } 174 | return wrapResponse(response, clazz, constructor); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/ClientBuilder.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import com.google.gson.Gson; 4 | import org.mineskin.options.*; 5 | import org.mineskin.request.RequestHandler; 6 | import org.mineskin.request.RequestHandlerConstructor; 7 | 8 | import java.util.concurrent.Executor; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.logging.Level; 12 | 13 | public class ClientBuilder { 14 | 15 | private static final int DEFAULT_GENERATE_QUEUE_INTERVAL = 200; 16 | private static final int DEFAULT_GENERATE_QUEUE_CONCURRENCY = 1; 17 | private static final int DEFAULT_GET_QUEUE_INTERVAL = 100; 18 | private static final int DEFAULT_GET_QUEUE_CONCURRENCY = 5; 19 | private static final int DEFAULT_JOB_CHECK_INTERVAL = 1000; 20 | private static final int DEFAULT_JOB_CHECK_INITIAL_DELAY = 2000; 21 | private static final int DEFAULT_JOB_CHECK_MAX_ATTEMPTS = 10; 22 | 23 | private String baseUrl = "https://api.mineskin.org"; 24 | private String userAgent = "MineSkinClient"; 25 | private String apiKey = null; 26 | private int timeout = 10000; 27 | private Gson gson = new Gson(); 28 | private Executor getExecutor = null; 29 | private Executor generateExecutor = null; 30 | private IQueueOptions generateQueueOptions = null; 31 | private IQueueOptions getQueueOptions = null; 32 | private IJobCheckOptions jobCheckOptions = null; 33 | private RequestHandlerConstructor requestHandlerConstructor = null; 34 | 35 | private ClientBuilder() { 36 | } 37 | 38 | /** 39 | * Create a new ClientBuilder 40 | */ 41 | public static ClientBuilder create() { 42 | return new ClientBuilder(); 43 | } 44 | 45 | /** 46 | * Set the base URL for the API 47 | * 48 | * @param baseUrl the base URL, e.g. "https://api.mineskin.org" 49 | */ 50 | public ClientBuilder baseUrl(String baseUrl) { 51 | this.baseUrl = baseUrl; 52 | return this; 53 | } 54 | 55 | /** 56 | * Set the User-Agent 57 | */ 58 | public ClientBuilder userAgent(String userAgent) { 59 | this.userAgent = userAgent; 60 | return this; 61 | } 62 | 63 | /** 64 | * Set the API key 65 | */ 66 | public ClientBuilder apiKey(String apiKey) { 67 | this.apiKey = apiKey; 68 | return this; 69 | } 70 | 71 | /** 72 | * Set the timeout 73 | */ 74 | public ClientBuilder timeout(int timeout) { 75 | this.timeout = timeout; 76 | return this; 77 | } 78 | 79 | /** 80 | * Set the Gson instance 81 | */ 82 | public ClientBuilder gson(Gson gson) { 83 | this.gson = gson; 84 | return this; 85 | } 86 | 87 | /** 88 | * Set the Executor for get requests 89 | */ 90 | public ClientBuilder getExecutor(Executor getExecutor) { 91 | this.getExecutor = getExecutor; 92 | return this; 93 | } 94 | 95 | /** 96 | * Set the Executor for generate requests 97 | */ 98 | public ClientBuilder generateExecutor(Executor generateExecutor) { 99 | this.generateExecutor = generateExecutor; 100 | return this; 101 | } 102 | 103 | /** 104 | * Set the ScheduledExecutorService for submitting queue jobs 105 | * 106 | * @deprecated use {@link #generateQueueOptions(IQueueOptions)} instead 107 | */ 108 | @Deprecated 109 | public ClientBuilder generateRequestScheduler(ScheduledExecutorService scheduledExecutor) { 110 | this.generateQueueOptions = new QueueOptions(scheduledExecutor, DEFAULT_GENERATE_QUEUE_INTERVAL, DEFAULT_GENERATE_QUEUE_CONCURRENCY); 111 | return this; 112 | } 113 | 114 | /** 115 | * Set the options for submitting queue jobs
116 | * defaults to 200ms interval and 1 concurrent request
117 | * For example: 118 | *
119 |      * {@code
120 |      * GenerateQueueOptions.create()
121 |      *         .withInterval(200, TimeUnit.MILLISECONDS)
122 |      *         .withConcurrency(2)
123 |      *  }
124 |      * 
125 | * 126 | * @see GenerateQueueOptions 127 | * @see QueueOptions 128 | */ 129 | public ClientBuilder generateQueueOptions(IQueueOptions queueOptions) { 130 | this.generateQueueOptions = queueOptions; 131 | return this; 132 | } 133 | 134 | /** 135 | * Set the ScheduledExecutorService for get requests, e.g. getting skins 136 | * 137 | * @deprecated use {@link #getQueueOptions(IQueueOptions)} instead 138 | */ 139 | @Deprecated 140 | public ClientBuilder getRequestScheduler(ScheduledExecutorService scheduledExecutor) { 141 | this.getQueueOptions = new QueueOptions(scheduledExecutor, DEFAULT_GET_QUEUE_INTERVAL, DEFAULT_GET_QUEUE_CONCURRENCY); 142 | return this; 143 | } 144 | 145 | /** 146 | * Set the options for get requests, e.g. getting skins
147 | * defaults to 100ms interval and 5 concurrent requests
148 | * For example: 149 | *
150 |      * {@code
151 |      * GetQueueOptions.create()
152 |      *         .withInterval(500, TimeUnit.MILLISECONDS)
153 |      *  }
154 |      * 
155 | * 156 | * @see GetQueueOptions 157 | * @see QueueOptions 158 | */ 159 | public ClientBuilder getQueueOptions(IQueueOptions queueOptions) { 160 | this.getQueueOptions = queueOptions; 161 | return this; 162 | } 163 | 164 | /** 165 | * Set the ScheduledExecutorService for checking job status 166 | * 167 | * @deprecated use {@link #jobCheckOptions(IJobCheckOptions)} instead 168 | */ 169 | @Deprecated 170 | public ClientBuilder jobCheckScheduler(ScheduledExecutorService scheduledExecutor) { 171 | this.jobCheckOptions = new JobCheckOptions(scheduledExecutor, DEFAULT_JOB_CHECK_INTERVAL, DEFAULT_JOB_CHECK_INITIAL_DELAY, DEFAULT_JOB_CHECK_MAX_ATTEMPTS, false); 172 | return this; 173 | } 174 | 175 | /** 176 | * Set the options for checking job status
177 | * defaults to 1000ms interval, 2000ms initial delay, and 10 max attempts
178 | * For example: 179 | *
180 |      * {@code
181 |      * JobCheckOptions.create()
182 |      *         .withInitialDelay(1000, TimeUnit.MILLISECONDS)
183 |      *         .withInterval(RequestInterval.exponential())
184 |      *         .withMaxAttempts(50)
185 |      * }
186 |      * 
187 | * 188 | * @see JobCheckOptions 189 | */ 190 | public ClientBuilder jobCheckOptions(IJobCheckOptions jobCheckOptions) { 191 | this.jobCheckOptions = jobCheckOptions; 192 | return this; 193 | } 194 | 195 | /** 196 | * Set the constructor for the RequestHandler 197 | */ 198 | public ClientBuilder requestHandler(RequestHandlerConstructor requestHandlerConstructor) { 199 | this.requestHandlerConstructor = requestHandlerConstructor; 200 | return this; 201 | } 202 | 203 | /** 204 | * Build the MineSkinClient 205 | */ 206 | public MineSkinClient build() { 207 | if (requestHandlerConstructor == null) { 208 | throw new IllegalStateException("RequestHandlerConstructor is not set"); 209 | } 210 | if ("MineSkinClient".equals(userAgent)) { 211 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Using default User-Agent: MineSkinClient - Please set a custom User-Agent (e.g. AppName/Version) to identify your application"); 212 | } 213 | if (apiKey == null || apiKey.isBlank()) { 214 | apiKey = null; 215 | MineSkinClientImpl.LOGGER.log(Level.WARNING, "Creating MineSkinClient without API key - Please get an API key from https://account.mineskin.org/keys"); 216 | } else if (apiKey.startsWith("msk_")) { 217 | String[] split = apiKey.split("_", 3); 218 | if (split.length == 3) { 219 | String id = split[1]; 220 | MineSkinClientImpl.LOGGER.log(Level.FINE, "Creating MineSkinClient with API key: " + id); 221 | } 222 | } 223 | 224 | if (getExecutor == null) { 225 | getExecutor = Executors.newSingleThreadExecutor(r -> { 226 | Thread thread = new Thread(r); 227 | thread.setName("MineSkinClient/get"); 228 | return thread; 229 | }); 230 | } 231 | if (generateExecutor == null) { 232 | generateExecutor = Executors.newSingleThreadExecutor(r -> { 233 | Thread thread = new Thread(r); 234 | thread.setName("MineSkinClient/generate"); 235 | return thread; 236 | }); 237 | } 238 | 239 | if (generateQueueOptions == null) { 240 | generateQueueOptions = new QueueOptions( 241 | Executors.newSingleThreadScheduledExecutor(r -> { 242 | Thread thread = new Thread(r); 243 | thread.setName("MineSkinClient/scheduler"); 244 | return thread; 245 | }), 246 | DEFAULT_GENERATE_QUEUE_INTERVAL, 247 | DEFAULT_GENERATE_QUEUE_CONCURRENCY 248 | ); 249 | } 250 | if (getQueueOptions == null) { 251 | getQueueOptions = new QueueOptions( 252 | generateQueueOptions.scheduler(), 253 | DEFAULT_GET_QUEUE_INTERVAL, 254 | DEFAULT_GET_QUEUE_CONCURRENCY 255 | ); 256 | } 257 | if (jobCheckOptions == null) { 258 | jobCheckOptions = new JobCheckOptions( 259 | generateQueueOptions.scheduler(), 260 | DEFAULT_JOB_CHECK_INTERVAL, 261 | DEFAULT_JOB_CHECK_INITIAL_DELAY, 262 | DEFAULT_JOB_CHECK_MAX_ATTEMPTS, 263 | false 264 | ); 265 | } 266 | 267 | RequestHandler requestHandler = requestHandlerConstructor.construct(baseUrl, userAgent, apiKey, timeout, gson); 268 | RequestExecutors executors = new RequestExecutors(getExecutor, generateExecutor, generateQueueOptions, getQueueOptions, jobCheckOptions); 269 | MineSkinClientImpl client = new MineSkinClientImpl(requestHandler, executors); 270 | if (executors.generateQueueOptions() instanceof AutoGenerateQueueOptions autoGenerate) { 271 | autoGenerate.setClient(client); 272 | } 273 | return client; 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /core/src/main/java/org/mineskin/MineSkinClientImpl.java: -------------------------------------------------------------------------------- 1 | package org.mineskin; 2 | 3 | import com.google.gson.JsonObject; 4 | import org.mineskin.data.*; 5 | import org.mineskin.exception.MineSkinRequestException; 6 | import org.mineskin.exception.MineskinException; 7 | import org.mineskin.options.IJobCheckOptions; 8 | import org.mineskin.request.*; 9 | import org.mineskin.request.source.UploadSource; 10 | import org.mineskin.response.*; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.net.URL; 15 | import java.util.Map; 16 | import java.util.UUID; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | 21 | import static com.google.common.base.Preconditions.checkNotNull; 22 | 23 | public class MineSkinClientImpl implements MineSkinClient { 24 | 25 | public static final Logger LOGGER = Logger.getLogger(MineSkinClient.class.getName()); 26 | 27 | private final RequestExecutors executors; 28 | 29 | private final RequestHandler requestHandler; 30 | private final RequestQueue generateQueue; 31 | private final RequestQueue getQueue; 32 | 33 | private final QueueClient queueClient = new QueueClientImpl(); 34 | private final GenerateClient generateClient = new GenerateClientImpl(); 35 | private final SkinsClient skinsClient = new SkinsClientImpl(); 36 | private final MiscClient miscClient = new MiscClientImpl(); 37 | 38 | public MineSkinClientImpl(RequestHandler requestHandler, RequestExecutors executors) { 39 | this.requestHandler = checkNotNull(requestHandler); 40 | this.executors = checkNotNull(executors); 41 | 42 | this.generateQueue = new RequestQueue(executors.generateQueueOptions()); 43 | this.getQueue = new RequestQueue(executors.getQueueOptions()); 44 | } 45 | 46 | @Override 47 | public Logger getLogger() { 48 | return LOGGER; 49 | } 50 | 51 | /// // 52 | 53 | 54 | @Override 55 | public QueueClient queue() { 56 | return queueClient; 57 | } 58 | 59 | @Override 60 | public GenerateClient generate() { 61 | return generateClient; 62 | } 63 | 64 | @Override 65 | public SkinsClient skins() { 66 | return skinsClient; 67 | } 68 | 69 | @Override 70 | public MiscClient misc() { 71 | return miscClient; 72 | } 73 | 74 | class QueueClientImpl implements QueueClient { 75 | 76 | @Override 77 | public CompletableFuture submit(GenerateRequest request) { 78 | if (request instanceof UploadRequestBuilder uploadRequestBuilder) { 79 | return queueUpload(uploadRequestBuilder); 80 | } else if (request instanceof UrlRequestBuilder urlRequestBuilder) { 81 | return queueUrl(urlRequestBuilder); 82 | } else if (request instanceof UserRequestBuilder userRequestBuilder) { 83 | return queueUser(userRequestBuilder); 84 | } 85 | throw new MineskinException("Unknown request builder type: " + request.getClass()); 86 | } 87 | 88 | CompletableFuture queueUpload(UploadRequestBuilder builder) { 89 | LOGGER.log(Level.FINER, "Adding upload request to internal queue: {0}", builder); 90 | return generateQueue.submit(() -> { 91 | try { 92 | Map data = builder.options().toMap(); 93 | UploadSource source = builder.getUploadSource(); 94 | checkNotNull(source); 95 | try (InputStream inputStream = source.getInputStream()) { 96 | LOGGER.log(Level.FINER, "Submitting to MineSkin queue: {0}", builder); 97 | QueueResponseImpl res = requestHandler.postFormDataFile("/v2/queue", "file", "mineskinjava", inputStream, data, JobInfo.class, QueueResponseImpl::new); 98 | handleGenerateResponse(res); 99 | return res; 100 | } 101 | } catch (IOException e) { 102 | throw new MineskinException(e); 103 | } catch (MineSkinRequestException e) { 104 | handleGenerateResponse(e.getResponse()); 105 | throw e; 106 | } 107 | }, executors.generateExecutor()); 108 | } 109 | 110 | CompletableFuture queueUrl(UrlRequestBuilder builder) { 111 | LOGGER.log(Level.FINER, "Adding url request to internal queue: {0}", builder); 112 | return generateQueue.submit(() -> { 113 | try { 114 | JsonObject body = builder.options().toJson(); 115 | URL url = builder.getUrl(); 116 | checkNotNull(url); 117 | body.addProperty("url", url.toString()); 118 | LOGGER.log(Level.FINER, "Submitting to MineSkin queue: {0}", builder); 119 | QueueResponseImpl res = requestHandler.postJson("/v2/queue", body, JobInfo.class, QueueResponseImpl::new); 120 | handleGenerateResponse(res); 121 | return res; 122 | } catch (IOException e) { 123 | throw new MineskinException(e); 124 | } catch (MineSkinRequestException e) { 125 | handleGenerateResponse(e.getResponse()); 126 | throw e; 127 | } 128 | }, executors.generateExecutor()); 129 | } 130 | 131 | CompletableFuture queueUser(UserRequestBuilder builder) { 132 | LOGGER.log(Level.FINER, "Adding user request to internal queue: {0}", builder); 133 | return generateQueue.submit(() -> { 134 | try { 135 | JsonObject body = builder.options().toJson(); 136 | UUID uuid = builder.getUuid(); 137 | checkNotNull(uuid); 138 | body.addProperty("user", uuid.toString()); 139 | LOGGER.log(Level.FINER, "Submitting to MineSkin queue: {0}", builder); 140 | QueueResponseImpl res = requestHandler.postJson("/v2/queue", body, JobInfo.class, QueueResponseImpl::new); 141 | handleGenerateResponse(res); 142 | return res; 143 | } catch (IOException e) { 144 | throw new MineskinException(e); 145 | } catch (MineSkinRequestException e) { 146 | handleGenerateResponse(e.getResponse()); 147 | throw e; 148 | } 149 | }, executors.generateExecutor()); 150 | } 151 | 152 | private void handleGenerateResponse(MineSkinResponse response0) { 153 | if (!(response0 instanceof QueueResponse response)) return; 154 | RateLimitInfo rateLimit = response.getRateLimit(); 155 | if (rateLimit == null) return; 156 | long nextRelative = rateLimit.next().relative(); 157 | if (nextRelative > 0) { 158 | generateQueue.setNextRequest(Math.max(generateQueue.getNextRequest(), System.currentTimeMillis() + nextRelative)); 159 | } 160 | } 161 | 162 | @Override 163 | public CompletableFuture get(JobInfo jobInfo) { 164 | checkNotNull(jobInfo); 165 | return get(jobInfo.id()); 166 | } 167 | 168 | @Override 169 | public CompletableFuture get(String id) { 170 | checkNotNull(id); 171 | return CompletableFuture.supplyAsync(() -> { 172 | try { 173 | return requestHandler.getJson("/v2/queue/" + id, JobInfo.class, JobResponseImpl::new); 174 | } catch (IOException e) { 175 | throw new MineskinException(e); 176 | } 177 | }, executors.getExecutor()); 178 | } 179 | 180 | @Override 181 | public CompletableFuture waitForCompletion(JobInfo jobInfo) { 182 | checkNotNull(jobInfo); 183 | if (jobInfo.id() == null) { 184 | return CompletableFuture.completedFuture(new NullJobReference(jobInfo)); 185 | } 186 | IJobCheckOptions options = executors.jobCheckOptions(); 187 | return new JobChecker(MineSkinClientImpl.this, jobInfo, options).check(); 188 | } 189 | 190 | 191 | } 192 | 193 | class GenerateClientImpl implements GenerateClient { 194 | 195 | @Override 196 | public CompletableFuture submitAndWait(GenerateRequest request) { 197 | if (request instanceof UploadRequestBuilder uploadRequestBuilder) { 198 | return generateUpload(uploadRequestBuilder); 199 | } else if (request instanceof UrlRequestBuilder urlRequestBuilder) { 200 | return generateUrl(urlRequestBuilder); 201 | } else if (request instanceof UserRequestBuilder userRequestBuilder) { 202 | return generateUser(userRequestBuilder); 203 | } 204 | throw new MineskinException("Unknown request builder type: " + request.getClass()); 205 | } 206 | 207 | CompletableFuture generateUpload(UploadRequestBuilder builder) { 208 | LOGGER.log(Level.FINER, "Adding upload request to internal generate queue: {0}", builder); 209 | return generateQueue.submit(() -> { 210 | try { 211 | Map data = builder.options().toMap(); 212 | UploadSource source = builder.getUploadSource(); 213 | checkNotNull(source); 214 | try (InputStream inputStream = source.getInputStream()) { 215 | LOGGER.log(Level.FINER, "Submitting to MineSkin generate: {0}", builder); 216 | GenerateResponseImpl res = requestHandler.postFormDataFile("/v2/generate", "file", "mineskinjava", inputStream, data, SkinInfo.class, GenerateResponseImpl::new); 217 | handleGenerateResponse(res); 218 | return res; 219 | } 220 | } catch (IOException e) { 221 | throw new MineskinException(e); 222 | } catch (MineSkinRequestException e) { 223 | handleGenerateResponse(e.getResponse()); 224 | throw e; 225 | } 226 | }, executors.generateExecutor()); 227 | } 228 | 229 | CompletableFuture generateUrl(UrlRequestBuilder builder) { 230 | LOGGER.log(Level.FINER, "Adding url request to internal generate queue: {0}", builder); 231 | return generateQueue.submit(() -> { 232 | try { 233 | JsonObject body = builder.options().toJson(); 234 | URL url = builder.getUrl(); 235 | checkNotNull(url); 236 | body.addProperty("url", url.toString()); 237 | LOGGER.log(Level.FINER, "Submitting to MineSkin generate: {0}", builder); 238 | GenerateResponseImpl res = requestHandler.postJson("/v2/generate", body, SkinInfo.class, GenerateResponseImpl::new); 239 | handleGenerateResponse(res); 240 | return res; 241 | } catch (IOException e) { 242 | throw new MineskinException(e); 243 | } catch (MineSkinRequestException e) { 244 | handleGenerateResponse(e.getResponse()); 245 | throw e; 246 | } 247 | }, executors.generateExecutor()); 248 | } 249 | 250 | CompletableFuture generateUser(UserRequestBuilder builder) { 251 | LOGGER.log(Level.FINER, "Adding user request to internal generate queue: {0}", builder); 252 | return generateQueue.submit(() -> { 253 | try { 254 | JsonObject body = builder.options().toJson(); 255 | UUID uuid = builder.getUuid(); 256 | checkNotNull(uuid); 257 | body.addProperty("user", uuid.toString()); 258 | LOGGER.log(Level.FINER, "Submitting to MineSkin generate: {0}", builder); 259 | GenerateResponseImpl res = requestHandler.postJson("/v2/generate", body, SkinInfo.class, GenerateResponseImpl::new); 260 | handleGenerateResponse(res); 261 | return res; 262 | } catch (IOException e) { 263 | throw new MineskinException(e); 264 | } catch (MineSkinRequestException e) { 265 | handleGenerateResponse(e.getResponse()); 266 | throw e; 267 | } 268 | }, executors.generateExecutor()); 269 | } 270 | 271 | private void handleGenerateResponse(MineSkinResponse response0) { 272 | LOGGER.log(Level.FINER, "Handling generate response: {0}", response0); 273 | if (!(response0 instanceof GenerateResponse response)) return; 274 | RateLimitInfo rateLimit = response.getRateLimit(); 275 | if (rateLimit == null) return; 276 | long nextRelative = rateLimit.next().relative(); 277 | if (nextRelative > 0) { 278 | generateQueue.setNextRequest(Math.max(generateQueue.getNextRequest(), System.currentTimeMillis() + nextRelative)); 279 | } 280 | } 281 | 282 | } 283 | 284 | class SkinsClientImpl implements SkinsClient { 285 | 286 | /** 287 | * Get an existing skin by UUID (Note: not the player's UUID) 288 | */ 289 | @Override 290 | public CompletableFuture get(UUID uuid) { 291 | checkNotNull(uuid); 292 | return get(uuid.toString()); 293 | } 294 | 295 | /** 296 | * Get an existing skin by UUID (Note: not the player's UUID) 297 | */ 298 | @Override 299 | public CompletableFuture get(String uuid) { 300 | checkNotNull(uuid); 301 | return getQueue.submit(() -> { 302 | try { 303 | return requestHandler.getJson("/v2/skins/" + uuid, SkinInfo.class, SkinResponseImpl::new); 304 | } catch (IOException e) { 305 | throw new MineskinException(e); 306 | } 307 | }, executors.getExecutor()); 308 | } 309 | 310 | } 311 | 312 | class MiscClientImpl implements MiscClient { 313 | @Override 314 | public CompletableFuture getUser() { 315 | return getQueue.submit(() -> { 316 | try { 317 | return requestHandler.getJson("/v2/me", UserInfo.class, UserResponseImpl::new); 318 | } catch (IOException e) { 319 | throw new MineskinException(e); 320 | } 321 | }, executors.getExecutor()); 322 | } 323 | } 324 | 325 | } 326 | -------------------------------------------------------------------------------- /tests/src/test/java/test/GenerateTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.Before; 4 | import org.junit.Ignore; 5 | import org.junit.Test; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.Arguments; 8 | import org.junit.jupiter.params.provider.MethodSource; 9 | import org.mineskin.*; 10 | import org.mineskin.data.*; 11 | import org.mineskin.exception.MineSkinRequestException; 12 | import org.mineskin.request.GenerateRequest; 13 | import org.mineskin.response.GenerateResponse; 14 | import org.mineskin.response.JobResponse; 15 | import org.mineskin.response.QueueResponse; 16 | 17 | import javax.imageio.ImageIO; 18 | import java.awt.image.BufferedImage; 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.text.SimpleDateFormat; 22 | import java.util.Date; 23 | import java.util.HashMap; 24 | import java.util.Iterator; 25 | import java.util.Map; 26 | import java.util.concurrent.CompletionException; 27 | import java.util.concurrent.Executor; 28 | import java.util.concurrent.Executors; 29 | import java.util.concurrent.ThreadLocalRandom; 30 | import java.util.logging.ConsoleHandler; 31 | import java.util.logging.Level; 32 | import java.util.stream.Stream; 33 | 34 | import static org.junit.jupiter.api.Assertions.*; 35 | 36 | public class GenerateTest { 37 | 38 | static { 39 | // set logger to log milliseconds 40 | System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT.%1$tL %4$s %2$s: %5$s%6$s%n"); 41 | 42 | MineSkinClientImpl.LOGGER.setLevel(Level.ALL); 43 | ConsoleHandler handler = new ConsoleHandler(); 44 | handler.setLevel(Level.ALL); 45 | MineSkinClientImpl.LOGGER.addHandler(handler); 46 | } 47 | 48 | private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(); 49 | 50 | private static final MineSkinClient APACHE = MineSkinClient.builder() 51 | .requestHandler(ApacheRequestHandler::new) 52 | .userAgent("MineSkinClient-Apache/Tests") 53 | .apiKey(System.getenv("MINESKIN_API_KEY")) 54 | .generateExecutor(EXECUTOR) 55 | .build(); 56 | private static final MineSkinClient JSOUP = MineSkinClient.builder() 57 | .requestHandler(JsoupRequestHandler::new) 58 | .userAgent("MineSkinClient-Jsoup/Tests") 59 | .apiKey(System.getenv("MINESKIN_API_KEY")) 60 | .generateExecutor(EXECUTOR) 61 | .build(); 62 | private static final MineSkinClient JAVA11 = MineSkinClient.builder() 63 | .requestHandler(Java11RequestHandler::new) 64 | .userAgent("MineSkinClient-Java11/Tests") 65 | .apiKey(System.getenv("MINESKIN_API_KEY")) 66 | .generateExecutor(EXECUTOR) 67 | .build(); 68 | 69 | @Before 70 | public void before() throws InterruptedException { 71 | Thread.sleep(1000); 72 | } 73 | 74 | private static Stream clients() { 75 | return Stream.of( 76 | Arguments.of(APACHE), 77 | Arguments.of(JSOUP), 78 | Arguments.of(JAVA11) 79 | ); 80 | } 81 | 82 | // @ParameterizedTest 83 | // @MethodSource("clients") 84 | // public void urlTest(MineSkinClient client) throws InterruptedException { 85 | // Thread.sleep(1000); 86 | // 87 | // final String name = "JavaClient-Url"; 88 | // try { 89 | // GenerateResponse res = client.generateUrl("https://i.imgur.com/jkhZKDX.png", GenerateOptions.create().name(name)).join(); 90 | // log(res); 91 | // Skin skin = res.getSkin(); 92 | // validateSkin(skin, name); 93 | // } catch (CompletionException e) { 94 | // if (e.getCause() instanceof MineSkinRequestException req) { 95 | // log(req.getResponse()); 96 | // } 97 | // throw e; 98 | // } 99 | // Thread.sleep(1000); 100 | // } 101 | 102 | @ParameterizedTest 103 | @MethodSource("clients") 104 | public void singleQueueUploadTest(MineSkinClient client) throws InterruptedException, IOException { 105 | Thread.sleep(1000); 106 | 107 | File file = File.createTempFile("mineskin-temp-upload-image", ".png"); 108 | ImageIO.write(ImageUtil.randomImage(64, ThreadLocalRandom.current().nextBoolean() ? 64 : 32), "png", file); 109 | log("#queueTest"); 110 | long start = System.currentTimeMillis(); 111 | try { 112 | String name = "mskjva-upl-" + ThreadLocalRandom.current().nextInt(1000); 113 | GenerateRequest request = GenerateRequest.upload(file) 114 | .visibility(Visibility.UNLISTED) 115 | .name(name); 116 | log("Submitting to queue: " + request); 117 | QueueResponse res = client.queue().submit(request).join(); 118 | log("Queue submit took " + (System.currentTimeMillis() - start) + "ms"); 119 | log(res); 120 | JobReference jobResponse = res.getBody().waitForCompletion(client).join(); 121 | log("Job took " + (System.currentTimeMillis() - start) + "ms"); 122 | log(jobResponse); 123 | SkinInfo skinInfo = jobResponse.getOrLoadSkin(client).join(); 124 | validateSkin(skinInfo, name); 125 | } catch (CompletionException e) { 126 | if (e.getCause() instanceof MineSkinRequestException req) { 127 | log(req.getResponse()); 128 | } 129 | throw e; 130 | } 131 | Thread.sleep(1000); 132 | } 133 | 134 | @ParameterizedTest 135 | @MethodSource("clients") 136 | public void singleQueueUrlTest(MineSkinClient client) throws InterruptedException, IOException { 137 | Thread.sleep(1000); 138 | 139 | long start = System.currentTimeMillis(); 140 | try { 141 | String name = "mskjva-url-" + ThreadLocalRandom.current().nextInt(1000); 142 | GenerateRequest request = GenerateRequest.url("https://api.mineskin.org/random-image?t=" + System.currentTimeMillis()) 143 | .visibility(Visibility.UNLISTED) 144 | .name(name); 145 | QueueResponse res = client.queue().submit(request).join(); 146 | log("Queue submit took " + (System.currentTimeMillis() - start) + "ms"); 147 | log(res); 148 | JobReference jobResponse = res.getBody().waitForCompletion(client).join(); 149 | log("Job took " + (System.currentTimeMillis() - start) + "ms"); 150 | log(jobResponse); 151 | SkinInfo skinInfo = jobResponse.getOrLoadSkin(client).join(); 152 | validateSkin(skinInfo, name); 153 | } catch (CompletionException e) { 154 | if (e.getCause() instanceof MineSkinRequestException req) { 155 | log(req.getResponse()); 156 | } 157 | throw e; 158 | } 159 | Thread.sleep(1000); 160 | } 161 | 162 | @Ignore 163 | @Test 164 | public void multiQueueRenderedUploadTest() throws InterruptedException, IOException { 165 | MineSkinClient client = JAVA11; 166 | int count = 5; 167 | Thread.sleep(1000); 168 | 169 | long start = System.currentTimeMillis(); 170 | Map jobs = new HashMap<>(); 171 | for (int i = 0; i < count; i++) { 172 | long jobStart = System.currentTimeMillis(); 173 | try { 174 | Thread.sleep(100); 175 | String name = "mskjva-upl-" + i + "-" + ThreadLocalRandom.current().nextInt(1000); 176 | BufferedImage image = ImageUtil.randomImage(64, ThreadLocalRandom.current().nextBoolean() ? 64 : 32); 177 | GenerateRequest request = GenerateRequest.upload(image) 178 | .visibility(Visibility.UNLISTED) 179 | .name(name); 180 | QueueResponse res = client.queue().submit(request).join(); 181 | log("Queue submit took " + (System.currentTimeMillis() - jobStart) + "ms"); 182 | log(res); 183 | jobs.put(name, res.getBody()); 184 | } catch (CompletionException e) { 185 | if (e.getCause() instanceof MineSkinRequestException req) { 186 | log(req.getResponse()); 187 | } 188 | throw e; 189 | } 190 | } 191 | 192 | Map completedJobs = new HashMap<>(); 193 | int jobsPending = 1; 194 | while (jobsPending > 0) { 195 | jobsPending = 0; 196 | Iterator> iterator = jobs.entrySet().iterator(); 197 | for (; iterator.hasNext(); ) { 198 | Map.Entry entry = iterator.next(); 199 | JobInfo jobInfo = entry.getValue(); 200 | JobResponse jobResponse = client.queue().get(jobInfo).join(); 201 | if (jobResponse.getJob().status().isPending()) { 202 | jobsPending++; 203 | } else { 204 | completedJobs.put(entry.getKey(), jobInfo); 205 | iterator.remove(); 206 | } 207 | } 208 | log("Jobs pending: " + jobsPending); 209 | Thread.sleep(1000); 210 | } 211 | 212 | for (Map.Entry entry : completedJobs.entrySet()) { 213 | String name = entry.getKey(); 214 | JobInfo jobInfo = entry.getValue(); 215 | JobResponse jobResponse = client.queue().get(jobInfo).join(); 216 | log("Job took " + (System.currentTimeMillis() - start) + "ms"); 217 | log(jobResponse); 218 | assertTrue(jobResponse.getJob().status().isDone()); 219 | assertTrue(jobResponse.getSkin().isPresent()); 220 | SkinInfo skinInfo = jobResponse.getOrLoadSkin(client).join(); 221 | validateSkin(skinInfo, name); 222 | } 223 | 224 | 225 | Thread.sleep(1000); 226 | } 227 | 228 | @ParameterizedTest 229 | @MethodSource("clients") 230 | public void duplicateQueueUrlTest(MineSkinClient client) throws InterruptedException, IOException { 231 | Thread.sleep(1000); 232 | 233 | long start = System.currentTimeMillis(); 234 | try { 235 | String name = "mskjva-url"; 236 | GenerateRequest request = GenerateRequest.url("https://i.imgur.com/ZC5PRM4.png") 237 | .visibility(Visibility.UNLISTED) 238 | .name(name); 239 | QueueResponse res = client.queue().submit(request).join(); 240 | log("Queue submit took " + (System.currentTimeMillis() - start) + "ms"); 241 | log(res); 242 | JobReference jobResponse = res.getBody().waitForCompletion(client).join(); 243 | log("Job took " + (System.currentTimeMillis() - start) + "ms"); 244 | log(jobResponse); 245 | SkinInfo skinInfo = jobResponse.getOrLoadSkin(client).join(); 246 | validateSkin(skinInfo, name); 247 | } catch (CompletionException e) { 248 | if (e.getCause() instanceof MineSkinRequestException req) { 249 | log(req.getResponse()); 250 | } 251 | throw e; 252 | } 253 | Thread.sleep(1000); 254 | } 255 | 256 | @ParameterizedTest 257 | @MethodSource("clients") 258 | public void singleGenerateUploadTest(MineSkinClient client) throws InterruptedException, IOException { 259 | Thread.sleep(1000); 260 | 261 | File file = File.createTempFile("mineskin-temp-upload-image", ".png"); 262 | ImageIO.write(ImageUtil.randomImage(64, ThreadLocalRandom.current().nextBoolean() ? 64 : 32), "png", file); 263 | log("#queueTest"); 264 | long start = System.currentTimeMillis(); 265 | try { 266 | String name = "mskjva-upl-" + ThreadLocalRandom.current().nextInt(1000); 267 | GenerateRequest request = GenerateRequest.upload(file) 268 | .visibility(Visibility.UNLISTED) 269 | .name(name); 270 | GenerateResponse res = client.generate().submitAndWait(request).join(); 271 | log("Generate took " + (System.currentTimeMillis() - start) + "ms"); 272 | log(res); 273 | SkinInfo skinInfo = res.getSkin(); 274 | validateSkin(skinInfo, name); 275 | } catch (CompletionException e) { 276 | if (e.getCause() instanceof MineSkinRequestException req) { 277 | log(req.getResponse()); 278 | } 279 | throw e; 280 | } 281 | Thread.sleep(1000); 282 | } 283 | 284 | /* 285 | @ParameterizedTest 286 | @MethodSource("clients") 287 | public void uploadTest(MineSkinClient client) throws InterruptedException, IOException { 288 | Thread.sleep(1000); 289 | 290 | final String name = "JavaClient-Upload"; 291 | File file = File.createTempFile("mineskin-temp-upload-image", ".png"); 292 | ImageIO.write(ImageUtil.randomImage(64, ThreadLocalRandom.current().nextBoolean() ? 64 : 32), "png", file); 293 | log("#uploadTest"); 294 | long start = System.currentTimeMillis(); 295 | try { 296 | GenerateResponse res = client.generateUpload(file, GenerateOptions.create().visibility(Visibility.UNLISTED).name(name)).join(); 297 | log("Upload took " + (System.currentTimeMillis() - start) + "ms"); 298 | log(res); 299 | Skin skin = res.getSkin(); 300 | validateSkin(skin, name); 301 | } catch (CompletionException e) { 302 | if (e.getCause() instanceof MineSkinRequestException req) { 303 | log(req.getResponse()); 304 | } 305 | throw e; 306 | } 307 | Thread.sleep(1000); 308 | } 309 | 310 | */ 311 | 312 | /* 313 | @ParameterizedTest 314 | @MethodSource("clients") 315 | public void uploadRenderedImageTest(MineSkinClient client) throws InterruptedException, IOException { 316 | Thread.sleep(1000); 317 | 318 | final String name = "JavaClient-Upload"; 319 | log("#uploadRenderedImageTest"); 320 | long start = System.currentTimeMillis(); 321 | try { 322 | GenerateResponse res = client.generateUpload(ImageUtil.randomImage(64, ThreadLocalRandom.current().nextBoolean() ? 64 : 32), GenerateOptions.create().visibility(Visibility.UNLISTED).name(name)).join(); 323 | log("Upload took " + (System.currentTimeMillis() - start) + "ms"); 324 | log(res); 325 | Skin skin = res.getSkin(); 326 | validateSkin(skin, name); 327 | } catch (CompletionException e) { 328 | if (e.getCause() instanceof MineSkinRequestException req) { 329 | log(req.getResponse()); 330 | } 331 | throw e; 332 | } 333 | Thread.sleep(1000); 334 | } 335 | // 336 | // @Test() 337 | // public void multiUploadTest() throws InterruptedException, IOException { 338 | // for (int i = 0; i < 50; i++) { 339 | // try { 340 | // uploadTest(); 341 | // uploadRenderedImageTest(); 342 | // } catch (Exception e) { 343 | // e.printStackTrace(); 344 | // } 345 | // } 346 | // } 347 | */ 348 | void validateSkin(Skin skin, String name) { 349 | assertNotNull(skin); 350 | assertNotNull(skin.texture()); 351 | assertNotNull(skin.texture().data()); 352 | assertNotNull(skin.texture().data().value()); 353 | assertNotNull(skin.texture().data().signature()); 354 | 355 | assertEquals(name, skin.name()); 356 | } 357 | 358 | static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); 359 | 360 | void log(Object message) { 361 | Date date = new Date(); 362 | System.out.println(String.format("[%s] %s", DATE_FORMAT.format(date), message)); 363 | } 364 | 365 | } 366 | --------------------------------------------------------------------------------