From 677f8b02b090018375ae1e352ceacad4cdfbcbc1 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:09:01 +0800 Subject: [PATCH 1/2] feat: alternative minetools API provider --- config/cli/config.json | 1 + config/plugin/config.yml | 1 + src/main/java/de/pdinklag/mcstats/Config.java | 10 +++ .../MinetoolsAPIPlayerProfileProvider.java | 35 +++++++++ .../java/de/pdinklag/mcstats/Updater.java | 7 +- .../pdinklag/mcstats/bukkit/BukkitConfig.java | 1 + .../de/pdinklag/mcstats/cli/JSONConfig.java | 1 + .../de/pdinklag/mcstats/minetools/API.java | 71 +++++++++++++++++++ .../minetools/APIRequestException.java | 21 ++++++ .../minetools/EmptyResponseException.java | 9 +++ 10 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java create mode 100644 src/main/java/de/pdinklag/mcstats/minetools/API.java create mode 100644 src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java create mode 100644 src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java diff --git a/config/cli/config.json b/config/cli/config.json index 574678a1..179278b0 100644 --- a/config/cli/config.json +++ b/config/cli/config.json @@ -28,6 +28,7 @@ "inactiveDays": 7, "updateInactive": false, "profileUpdateInterval": 3, + "profileAPI": "mojang", "minPlaytime": 60, "excludeBanned": true, "excludeOps": false, diff --git a/config/plugin/config.yml b/config/plugin/config.yml index 936e80c3..7e3ece85 100644 --- a/config/plugin/config.yml +++ b/config/plugin/config.yml @@ -19,6 +19,7 @@ players: inactiveDays: 90 updateInactive: true profileUpdateInterval: 3 + profileAPI: "mojang" minPlaytime: 60 excludeBanned: true excludeOps: false diff --git a/src/main/java/de/pdinklag/mcstats/Config.java b/src/main/java/de/pdinklag/mcstats/Config.java index b3119b24..81c40457 100644 --- a/src/main/java/de/pdinklag/mcstats/Config.java +++ b/src/main/java/de/pdinklag/mcstats/Config.java @@ -31,6 +31,8 @@ public class Config { private int playerCacheUUIDPrefix = 2; private String defaultLanguage = "en"; + private String profileAPI = "mojang"; + public Config() { } @@ -177,4 +179,12 @@ public Path getEventsPath() { public void setEventsPath(Path eventsPath) { this.eventsPath = eventsPath; } + + public String getProfileAPI() { + return profileAPI; + } + + public void setProfileAPI(String profileAPI) { + this.profileAPI = profileAPI; + } } diff --git a/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java b/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java new file mode 100644 index 00000000..d4437863 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java @@ -0,0 +1,35 @@ +package de.pdinklag.mcstats; + +import de.pdinklag.mcstats.minetools.API; +import de.pdinklag.mcstats.minetools.APIRequestException; +import de.pdinklag.mcstats.minetools.EmptyResponseException; + +/** + * Provides player profiles via the Minetools.eu API. + */ +public class MinetoolsAPIPlayerProfileProvider implements PlayerProfileProvider { + private final LogWriter log; + + /** + * Constructs a new provider. + */ + public MinetoolsAPIPlayerProfileProvider(LogWriter log) { + this.log = log; + } + + @Override + public PlayerProfile getPlayerProfile(Player player) { + if (player.getAccountType().maybeMojangAccount()) { + try { + PlayerProfile profile = API.requestPlayerProfile(player.getUuid()); + player.setAccountType(AccountType.MOJANG); + return profile; + } catch (EmptyResponseException e) { + player.setAccountType(AccountType.OFFLINE); + } catch (APIRequestException e) { + log.writeError("Minetools.eu API profile request for player failed: " + player.getUuid(), e); + } + } + return player.getProfile(); + } +} diff --git a/src/main/java/de/pdinklag/mcstats/Updater.java b/src/main/java/de/pdinklag/mcstats/Updater.java index c6760248..e447f75b 100644 --- a/src/main/java/de/pdinklag/mcstats/Updater.java +++ b/src/main/java/de/pdinklag/mcstats/Updater.java @@ -60,7 +60,12 @@ public abstract class Updater { private final Path dbPlayerlistPath; protected PlayerProfileProvider getAuthenticProfileProvider() { - return new MojangAPIPlayerProfileProvider(log); + final String api = config.getProfileAPI(); + if ("minetools".equalsIgnoreCase(api)) { + return new MinetoolsAPIPlayerProfileProvider(log); + } else { + return new MojangAPIPlayerProfileProvider(log); + } } protected void gatherLocalProfileProviders(PlayerProfileProviderList providers) { diff --git a/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java b/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java index 3acd443e..d6cd6d01 100644 --- a/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java +++ b/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java @@ -38,6 +38,7 @@ public BukkitConfig(Plugin plugin) { setMinPlaytime(bukkitConfig.getInt("players.minPlaytime", getMinPlaytime())); setUpdateInactive(bukkitConfig.getBoolean("players.updateInactive", isUpdateInactive())); setProfileUpdateInterval(bukkitConfig.getInt("players.profileUpdateInterval", getProfileUpdateInterval())); + setProfileAPI(bukkitConfig.getString("players.profileAPI", getProfileAPI())); setExcludeBanned(bukkitConfig.getBoolean("players.excludeBanned", isExcludeBanned())); setExcludeOps(bukkitConfig.getBoolean("players.excludeOps", isExcludeOps())); diff --git a/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java b/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java index f26de97d..3be4c5eb 100644 --- a/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java +++ b/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java @@ -44,6 +44,7 @@ public JSONConfig(Path jsonPath) throws IOException, JSONException { setMinPlaytime(players.optInt("minPlaytime", getMinPlaytime())); setUpdateInactive(players.optBoolean("updateInactive", isUpdateInactive())); setProfileUpdateInterval(players.optInt("profileUpdateInterval", getProfileUpdateInterval())); + setProfileAPI(players.optString("profileAPI", getProfileAPI())); setExcludeBanned(players.optBoolean("excludeBanned", isExcludeBanned())); setExcludeOps(players.optBoolean("excludeOps", isExcludeOps())); diff --git a/src/main/java/de/pdinklag/mcstats/minetools/API.java b/src/main/java/de/pdinklag/mcstats/minetools/API.java new file mode 100644 index 00000000..fcf403f7 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/minetools/API.java @@ -0,0 +1,71 @@ +package de.pdinklag.mcstats.minetools; + +import java.net.URL; + +import javax.net.ssl.HttpsURLConnection; + +import org.json.JSONObject; + +import de.pdinklag.mcstats.PlayerProfile; +import de.pdinklag.mcstats.util.StreamUtils; + +/** + * Minetools.eu API. + */ +public class API { + private static final String API_URL = "https://api.minetools.eu/profile/"; + private static final String SKIN_URL = "http://textures.minecraft.net/texture/"; + + /** + * Requests a player profile from the Minetools.eu API. + * + * @param uuid the UUID of the player in question + * @return the player profile associated to the given UUID. + * @throws EmptyResponseException in case the Minetools.eu API gives an empty response + * @throws APIRequestException in case any error occurs trying to request the + * profile + */ + public static PlayerProfile requestPlayerProfile(String uuid) throws EmptyResponseException, APIRequestException { + try { + final String response; + { + URL url = new URL(API_URL + uuid); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + response = StreamUtils.readStreamFully(conn.getInputStream()); + conn.disconnect(); + } + + if (!response.isEmpty()) { + JSONObject obj = new JSONObject(response); + + // Check if the response has the decoded profile + if (!obj.has("decoded")) { + throw new EmptyResponseException(); + } + + JSONObject decoded = obj.getJSONObject("decoded"); + String name = decoded.getString("profileName"); + + // Extract skin URL from decoded textures + JSONObject textures = decoded.getJSONObject("textures"); + if (!textures.has("SKIN")) { + throw new EmptyResponseException(); + } + + String skinUrl = textures.getJSONObject("SKIN").getString("url"); + String skin = skinUrl.substring(SKIN_URL.length()); + + // Use timestamp from decoded profile, or current time if not available + long timestamp = decoded.optLong("timestamp", System.currentTimeMillis()); + + return new PlayerProfile(name, skin, timestamp); + } else { + throw new EmptyResponseException(); + } + } catch (EmptyResponseException e) { + throw e; // nb: delegate + } catch (Exception e) { + throw new APIRequestException(e); + } + } +} diff --git a/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java b/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java new file mode 100644 index 00000000..4928bb18 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java @@ -0,0 +1,21 @@ +package de.pdinklag.mcstats.minetools; + +/** + * An exception raised while processing a Minetools.eu API request. + */ +public class APIRequestException extends RuntimeException { + APIRequestException() { + } + + APIRequestException(String message) { + super(message); + } + + APIRequestException(Throwable cause) { + super(cause); + } + + APIRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java b/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java new file mode 100644 index 00000000..b3ffffe8 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java @@ -0,0 +1,9 @@ +package de.pdinklag.mcstats.minetools; + +/** + * Indicates an empty response from the Minetools.eu API. + */ +public class EmptyResponseException extends RuntimeException { + EmptyResponseException() { + } +} From 6fce9c24c2e64cd23940e838183c1006b04ba0e4 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:15:58 +0800 Subject: [PATCH 2/2] switch to playerdb.co, minetools has been offline for a bit now --- .DS_Store | Bin 0 -> 8196 bytes ... => PlayerDBAPIPlayerProfileProvider.java} | 14 ++-- .../java/de/pdinklag/mcstats/Updater.java | 4 +- .../de/pdinklag/mcstats/minetools/API.java | 71 ------------------ .../de/pdinklag/mcstats/playerdb/API.java | 70 +++++++++++++++++ .../APIRequestException.java | 4 +- .../EmptyResponseException.java | 4 +- 7 files changed, 83 insertions(+), 84 deletions(-) create mode 100644 .DS_Store rename src/main/java/de/pdinklag/mcstats/{MinetoolsAPIPlayerProfileProvider.java => PlayerDBAPIPlayerProfileProvider.java} (59%) delete mode 100644 src/main/java/de/pdinklag/mcstats/minetools/API.java create mode 100644 src/main/java/de/pdinklag/mcstats/playerdb/API.java rename src/main/java/de/pdinklag/mcstats/{minetools => playerdb}/APIRequestException.java (76%) rename src/main/java/de/pdinklag/mcstats/{minetools => playerdb}/EmptyResponseException.java (53%) diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9d9c04938404410e94c525f994bc3e2de7604848 GIT binary patch literal 8196 zcmeHM&2JM&6o2DBB83nI6>5ck|>Ts0txQ!dSbTO%vihY z7>FYIUM_G4_1bH1hzmz9Tzfdhqrj)306w!t zvOe(KSFNTr3K#`GO9jOJ!NDxBsIje3-a4?5ApqhaR?CDo#sQM!Xe?@ME0k31Q$-I% zQ;7~Sh@xY=Bis>-8rupL9f+a>(IXQbp%6Jb@*L?7q^Qu8MggNhzXBq59|8}W5J1=! zzl)7j!aV!0Op==N^jdL4NBS?kMi|6Nq3|ck3=9qpk60sC&f2cr=dDWOCe66yMt8-x zHO@n?W4qpZKwDLNVv)1N4H%6iA*fMw*}5ID8gG?&lhtC$t*8boYh|nU^ufX0d|_e6 zS-4y{oN*2=e_NPw=D%AwJj_~?`AgrgR(Hca#($NQ2BPm7a7ukYDj%bjT2kQe>m`vl zoh7}q?ardIU*twleR z;)-sv;38awpI{w!;SoH6XYf0`gxByE{(^tV2$?1q$VDpb8r*Nun5=S7F4jDg#;cz(!+Y%!^*-c`d)=4)I@OhdiQ5WJOtBo8h{w* z?|1i`iF&+-&?~`JCJ=Xf%KiR4wcm5F4-|WhagAcF@13_)Ce0dvmiTn{Ej2mzGS7d! zC?**NPEdgXHTIN<|F3=c{r?Hh&2? nLP^0wxQKcgjw2rZVTiV)+lm_73b6%~IS5D@OkouGs0#cCBi}uy literal 0 HcmV?d00001 diff --git a/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java b/src/main/java/de/pdinklag/mcstats/PlayerDBAPIPlayerProfileProvider.java similarity index 59% rename from src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java rename to src/main/java/de/pdinklag/mcstats/PlayerDBAPIPlayerProfileProvider.java index d4437863..10dc16b0 100644 --- a/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java +++ b/src/main/java/de/pdinklag/mcstats/PlayerDBAPIPlayerProfileProvider.java @@ -1,19 +1,19 @@ package de.pdinklag.mcstats; -import de.pdinklag.mcstats.minetools.API; -import de.pdinklag.mcstats.minetools.APIRequestException; -import de.pdinklag.mcstats.minetools.EmptyResponseException; +import de.pdinklag.mcstats.playerdb.API; +import de.pdinklag.mcstats.playerdb.APIRequestException; +import de.pdinklag.mcstats.playerdb.EmptyResponseException; /** - * Provides player profiles via the Minetools.eu API. + * Provides player profiles via the PlayerDB API. */ -public class MinetoolsAPIPlayerProfileProvider implements PlayerProfileProvider { +public class PlayerDBAPIPlayerProfileProvider implements PlayerProfileProvider { private final LogWriter log; /** * Constructs a new provider. */ - public MinetoolsAPIPlayerProfileProvider(LogWriter log) { + public PlayerDBAPIPlayerProfileProvider(LogWriter log) { this.log = log; } @@ -27,7 +27,7 @@ public PlayerProfile getPlayerProfile(Player player) { } catch (EmptyResponseException e) { player.setAccountType(AccountType.OFFLINE); } catch (APIRequestException e) { - log.writeError("Minetools.eu API profile request for player failed: " + player.getUuid(), e); + log.writeError("PlayerDB API profile request for player failed: " + player.getUuid(), e); } } return player.getProfile(); diff --git a/src/main/java/de/pdinklag/mcstats/Updater.java b/src/main/java/de/pdinklag/mcstats/Updater.java index e447f75b..189738ca 100644 --- a/src/main/java/de/pdinklag/mcstats/Updater.java +++ b/src/main/java/de/pdinklag/mcstats/Updater.java @@ -61,8 +61,8 @@ public abstract class Updater { protected PlayerProfileProvider getAuthenticProfileProvider() { final String api = config.getProfileAPI(); - if ("minetools".equalsIgnoreCase(api)) { - return new MinetoolsAPIPlayerProfileProvider(log); + if ("playerdb".equalsIgnoreCase(api)) { + return new PlayerDBAPIPlayerProfileProvider(log); } else { return new MojangAPIPlayerProfileProvider(log); } diff --git a/src/main/java/de/pdinklag/mcstats/minetools/API.java b/src/main/java/de/pdinklag/mcstats/minetools/API.java deleted file mode 100644 index fcf403f7..00000000 --- a/src/main/java/de/pdinklag/mcstats/minetools/API.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.pdinklag.mcstats.minetools; - -import java.net.URL; - -import javax.net.ssl.HttpsURLConnection; - -import org.json.JSONObject; - -import de.pdinklag.mcstats.PlayerProfile; -import de.pdinklag.mcstats.util.StreamUtils; - -/** - * Minetools.eu API. - */ -public class API { - private static final String API_URL = "https://api.minetools.eu/profile/"; - private static final String SKIN_URL = "http://textures.minecraft.net/texture/"; - - /** - * Requests a player profile from the Minetools.eu API. - * - * @param uuid the UUID of the player in question - * @return the player profile associated to the given UUID. - * @throws EmptyResponseException in case the Minetools.eu API gives an empty response - * @throws APIRequestException in case any error occurs trying to request the - * profile - */ - public static PlayerProfile requestPlayerProfile(String uuid) throws EmptyResponseException, APIRequestException { - try { - final String response; - { - URL url = new URL(API_URL + uuid); - HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); - response = StreamUtils.readStreamFully(conn.getInputStream()); - conn.disconnect(); - } - - if (!response.isEmpty()) { - JSONObject obj = new JSONObject(response); - - // Check if the response has the decoded profile - if (!obj.has("decoded")) { - throw new EmptyResponseException(); - } - - JSONObject decoded = obj.getJSONObject("decoded"); - String name = decoded.getString("profileName"); - - // Extract skin URL from decoded textures - JSONObject textures = decoded.getJSONObject("textures"); - if (!textures.has("SKIN")) { - throw new EmptyResponseException(); - } - - String skinUrl = textures.getJSONObject("SKIN").getString("url"); - String skin = skinUrl.substring(SKIN_URL.length()); - - // Use timestamp from decoded profile, or current time if not available - long timestamp = decoded.optLong("timestamp", System.currentTimeMillis()); - - return new PlayerProfile(name, skin, timestamp); - } else { - throw new EmptyResponseException(); - } - } catch (EmptyResponseException e) { - throw e; // nb: delegate - } catch (Exception e) { - throw new APIRequestException(e); - } - } -} diff --git a/src/main/java/de/pdinklag/mcstats/playerdb/API.java b/src/main/java/de/pdinklag/mcstats/playerdb/API.java new file mode 100644 index 00000000..2b205cb7 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/playerdb/API.java @@ -0,0 +1,70 @@ +package de.pdinklag.mcstats.playerdb; + +import java.net.URI; + +import javax.net.ssl.HttpsURLConnection; + +import org.json.JSONObject; + +import de.pdinklag.mcstats.PlayerProfile; +import de.pdinklag.mcstats.util.StreamUtils; + +/** + * PlayerDB API. + */ +public class API { + private static final String API_URL = "https://playerdb.co/api/player/minecraft/"; + private static final String TEXTURES_URL_PREFIX = "https://textures.minecraft.net/texture/"; + private static final String USER_AGENT = "MinecraftStats"; + + /** + * Requests a player profile from the PlayerDB API. + * + * @param id the UUID or username of the player in question + * @return the player profile associated to the given id + * @throws EmptyResponseException in case the PlayerDB API gives an empty response + * @throws APIRequestException in case any error occurs trying to request the + * profile + */ + public static PlayerProfile requestPlayerProfile(String id) throws EmptyResponseException, APIRequestException { + try { + final String response; + { + URI uri = URI.create(API_URL + id); + java.net.URL url = uri.toURL(); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", USER_AGENT); + response = StreamUtils.readStreamFully(conn.getInputStream()); + conn.disconnect(); + } + + if (response.isEmpty()) { + throw new EmptyResponseException(); + } + + JSONObject root = new JSONObject(response); + JSONObject data = root.optJSONObject("data"); + JSONObject player = (data == null) ? null : data.optJSONObject("player"); + if (player == null) { + throw new EmptyResponseException(); + } + + String name = player.optString("username", ""); + if (name.isEmpty()) { + throw new EmptyResponseException(); + } + + String skin = null; + String skinTexture = player.optString("skin_texture", ""); + if (skinTexture.startsWith(TEXTURES_URL_PREFIX) && skinTexture.length() > TEXTURES_URL_PREFIX.length()) { + skin = skinTexture.substring(TEXTURES_URL_PREFIX.length()); + } + + return new PlayerProfile(name, skin, System.currentTimeMillis()); + } catch (EmptyResponseException e) { + throw e; // nb: delegate + } catch (Exception e) { + throw new APIRequestException(e); + } + } +} diff --git a/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java b/src/main/java/de/pdinklag/mcstats/playerdb/APIRequestException.java similarity index 76% rename from src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java rename to src/main/java/de/pdinklag/mcstats/playerdb/APIRequestException.java index 4928bb18..4b5a2ac4 100644 --- a/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java +++ b/src/main/java/de/pdinklag/mcstats/playerdb/APIRequestException.java @@ -1,7 +1,7 @@ -package de.pdinklag.mcstats.minetools; +package de.pdinklag.mcstats.playerdb; /** - * An exception raised while processing a Minetools.eu API request. + * An exception raised while processing a PlayerDB API request. */ public class APIRequestException extends RuntimeException { APIRequestException() { diff --git a/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java b/src/main/java/de/pdinklag/mcstats/playerdb/EmptyResponseException.java similarity index 53% rename from src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java rename to src/main/java/de/pdinklag/mcstats/playerdb/EmptyResponseException.java index b3ffffe8..a457dc12 100644 --- a/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java +++ b/src/main/java/de/pdinklag/mcstats/playerdb/EmptyResponseException.java @@ -1,7 +1,7 @@ -package de.pdinklag.mcstats.minetools; +package de.pdinklag.mcstats.playerdb; /** - * Indicates an empty response from the Minetools.eu API. + * Indicates an empty response from the PlayerDB API. */ public class EmptyResponseException extends RuntimeException { EmptyResponseException() {