From f0f523b36196741d29362a81e311de59a7309047 Mon Sep 17 00:00:00 2001 From: Vinrobot Date: Tue, 22 Aug 2023 22:20:46 +0200 Subject: [PATCH] Add BetterTTV emotes --- .../mcemote/client/providers/BTTVEmote.java | 62 +++++++++++++++++++ .../providers/BTTVGlobalEmoteProvider.java | 25 ++++++++ .../providers/BTTVUserEmoteProvider.java | 27 ++++++++ ...ot.mcemote.client.providers.IEmoteProvider | 2 + .../mcemote/api/bttv/BetterTTVService.java | 45 ++++++++++++++ .../net/vinrobot/mcemote/api/bttv/Emote.java | 11 ++++ .../vinrobot/mcemote/api/bttv/Provider.java | 12 ++++ .../net/vinrobot/mcemote/api/bttv/User.java | 9 +++ .../mcemote/api/bttv/UserResponse.java | 10 +++ 9 files changed, 203 insertions(+) create mode 100644 src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java create mode 100644 src/client/java/net/vinrobot/mcemote/client/providers/BTTVGlobalEmoteProvider.java create mode 100644 src/client/java/net/vinrobot/mcemote/client/providers/BTTVUserEmoteProvider.java create mode 100644 src/main/java/net/vinrobot/mcemote/api/bttv/BetterTTVService.java create mode 100644 src/main/java/net/vinrobot/mcemote/api/bttv/Emote.java create mode 100644 src/main/java/net/vinrobot/mcemote/api/bttv/Provider.java create mode 100644 src/main/java/net/vinrobot/mcemote/api/bttv/User.java create mode 100644 src/main/java/net/vinrobot/mcemote/api/bttv/UserResponse.java diff --git a/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java new file mode 100644 index 0000000..0d5c363 --- /dev/null +++ b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java @@ -0,0 +1,62 @@ +package net.vinrobot.mcemote.client.providers; + +import net.minecraft.client.texture.NativeImage; +import net.vinrobot.mcemote.api.bttv.Emote; +import net.vinrobot.mcemote.client.helpers.NativeImageHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.util.Objects; + +public class BTTVEmote implements net.vinrobot.mcemote.client.font.Emote { + private static final Logger LOGGER = LoggerFactory.getLogger(BTTVEmote.class); + private static final int DEFAULT_SIZE = 28; + + private final Emote emote; + + public BTTVEmote(Emote emote) { + this.emote = emote; + } + + @Override + public String getName() { + return this.emote.code(); + } + + @Override + public int getWidth() { + final int width = this.emote.width(); + return width > 0 ? width : DEFAULT_SIZE; + } + + @Override + public int getHeight() { + final int height = this.emote.height(); + return height > 0 ? height : DEFAULT_SIZE; + } + + @Override + public Frame[] loadFrames() throws IOException { + if (this.emote.animated()) { + LOGGER.error("Animated BTTV emotes are not supported yet."); + } + + final URL url = new URL("https://cdn.betterttv.net/emote/" + this.emote.id() + "/1x"); + final BufferedImage image = Objects.requireNonNull(ImageIO.read(url)); + + final int expectedWidth = getWidth(), expectedHeight = getHeight(); + final int actualWidth = image.getWidth(), actualHeight = image.getHeight(); + if (expectedWidth != actualWidth || expectedHeight != actualHeight) { + final String expectedSize = expectedWidth + "x" + expectedHeight; + final String actualSize = actualWidth + "x" + actualHeight; + LOGGER.error("BTTV emote " + getName() + " has unexpected size " + actualSize + " (expected " + expectedSize + ")."); + } + + final NativeImage nativeImage = NativeImageHelper.fromBufferedImage(image); + return new Frame[]{new Frame(nativeImage)}; + } +} diff --git a/src/client/java/net/vinrobot/mcemote/client/providers/BTTVGlobalEmoteProvider.java b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVGlobalEmoteProvider.java new file mode 100644 index 0000000..2a57007 --- /dev/null +++ b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVGlobalEmoteProvider.java @@ -0,0 +1,25 @@ +package net.vinrobot.mcemote.client.providers; + +import net.vinrobot.mcemote.api.bttv.BetterTTVService; +import net.vinrobot.mcemote.api.bttv.Emote; +import net.vinrobot.mcemote.config.Configuration; + +import java.util.Arrays; + +public class BTTVGlobalEmoteProvider implements IEmoteProvider { + @Override + public int priority() { + return 10; + } + + @Override + public void registerEmotes(final Configuration config, final IEmoteRegistry registry) throws Exception { + final BetterTTVService service = new BetterTTVService(); + + final Emote[] emoteSets = service.fetchGlobalEmoteSet(); + + Arrays.stream(emoteSets) + .map(BTTVEmote::new) + .forEach(registry::registerEmote); + } +} diff --git a/src/client/java/net/vinrobot/mcemote/client/providers/BTTVUserEmoteProvider.java b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVUserEmoteProvider.java new file mode 100644 index 0000000..a883656 --- /dev/null +++ b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVUserEmoteProvider.java @@ -0,0 +1,27 @@ +package net.vinrobot.mcemote.client.providers; + +import net.vinrobot.mcemote.api.bttv.BetterTTVService; +import net.vinrobot.mcemote.api.bttv.Provider; +import net.vinrobot.mcemote.api.bttv.UserResponse; +import net.vinrobot.mcemote.config.Configuration; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class BTTVUserEmoteProvider implements IEmoteProvider { + @Override + public void registerEmotes(final Configuration config, final IEmoteRegistry registry) throws Exception { + final String twitchId = config.twitchId().get(); + if (twitchId.isEmpty()) { + return; + } + + final BetterTTVService service = new BetterTTVService(); + + final UserResponse emoteSets = service.fetchUserEmoteSet(Provider.TWITCH, twitchId); + + Stream.concat(Arrays.stream(emoteSets.channelEmotes()), Arrays.stream(emoteSets.sharedEmotes())) + .map(BTTVEmote::new) + .forEach(registry::registerEmote); + } +} diff --git a/src/client/resources/META-INF/services/net.vinrobot.mcemote.client.providers.IEmoteProvider b/src/client/resources/META-INF/services/net.vinrobot.mcemote.client.providers.IEmoteProvider index 24b6846..8371480 100644 --- a/src/client/resources/META-INF/services/net.vinrobot.mcemote.client.providers.IEmoteProvider +++ b/src/client/resources/META-INF/services/net.vinrobot.mcemote.client.providers.IEmoteProvider @@ -2,3 +2,5 @@ net.vinrobot.mcemote.client.providers.STVGlobalEmoteProvider net.vinrobot.mcemote.client.providers.STVUserEmoteProvider net.vinrobot.mcemote.client.providers.FFZGlobalEmoteProvider net.vinrobot.mcemote.client.providers.FFZRoomEmoteProvider +net.vinrobot.mcemote.client.providers.BTTVGlobalEmoteProvider +net.vinrobot.mcemote.client.providers.BTTVUserEmoteProvider diff --git a/src/main/java/net/vinrobot/mcemote/api/bttv/BetterTTVService.java b/src/main/java/net/vinrobot/mcemote/api/bttv/BetterTTVService.java new file mode 100644 index 0000000..d71eefa --- /dev/null +++ b/src/main/java/net/vinrobot/mcemote/api/bttv/BetterTTVService.java @@ -0,0 +1,45 @@ +package net.vinrobot.mcemote.api.bttv; + +import com.google.gson.Gson; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class BetterTTVService { + public static final String BASE_API = "https://api.betterttv.net/3/cached"; + public static final URI GLOBAL_EMOTE_SET_URI = URI.create(BASE_API + "/emotes/global"); + + private final HttpClient httpClient; + private final Gson gson = new Gson(); + + public BetterTTVService() { + this(HttpClient.newHttpClient()); + } + + public BetterTTVService(HttpClient httpClient) { + this.httpClient = httpClient; + } + + public Emote[] fetchGlobalEmoteSet() throws IOException, InterruptedException { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(GLOBAL_EMOTE_SET_URI) + .build(); + + HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + + return gson.fromJson(httpResponse.body(), Emote[].class); + } + + public UserResponse fetchUserEmoteSet(Provider provider, String userId) throws IOException, InterruptedException { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create(BASE_API + "/users/" + provider.path + "/" + userId)) + .build(); + + HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + + return gson.fromJson(httpResponse.body(), UserResponse.class); + } +} diff --git a/src/main/java/net/vinrobot/mcemote/api/bttv/Emote.java b/src/main/java/net/vinrobot/mcemote/api/bttv/Emote.java new file mode 100644 index 0000000..5e0aa08 --- /dev/null +++ b/src/main/java/net/vinrobot/mcemote/api/bttv/Emote.java @@ -0,0 +1,11 @@ +package net.vinrobot.mcemote.api.bttv; + +public record Emote( + String id, + String code, + String imageType, + boolean animated, + int width, + int height +) { +} diff --git a/src/main/java/net/vinrobot/mcemote/api/bttv/Provider.java b/src/main/java/net/vinrobot/mcemote/api/bttv/Provider.java new file mode 100644 index 0000000..dcb8ccf --- /dev/null +++ b/src/main/java/net/vinrobot/mcemote/api/bttv/Provider.java @@ -0,0 +1,12 @@ +package net.vinrobot.mcemote.api.bttv; + +public enum Provider { + TWITCH("twitch"), + YOUTUBE("youtube"); + + public final String path; + + Provider(String path) { + this.path = path; + } +} diff --git a/src/main/java/net/vinrobot/mcemote/api/bttv/User.java b/src/main/java/net/vinrobot/mcemote/api/bttv/User.java new file mode 100644 index 0000000..ec261df --- /dev/null +++ b/src/main/java/net/vinrobot/mcemote/api/bttv/User.java @@ -0,0 +1,9 @@ +package net.vinrobot.mcemote.api.bttv; + +public record User( + String id, + String name, + String displayName, + String providerId +) { +} diff --git a/src/main/java/net/vinrobot/mcemote/api/bttv/UserResponse.java b/src/main/java/net/vinrobot/mcemote/api/bttv/UserResponse.java new file mode 100644 index 0000000..0aba6d1 --- /dev/null +++ b/src/main/java/net/vinrobot/mcemote/api/bttv/UserResponse.java @@ -0,0 +1,10 @@ +package net.vinrobot.mcemote.api.bttv; + +public record UserResponse( + String id, + String[] bots, + String avatar, + Emote[] channelEmotes, + Emote[] sharedEmotes +) { +}