diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 070eb8d3..3cf77892 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -52,6 +52,7 @@ import java.util.Scanner; import javax.imageio.ImageIO; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.lwjgl.BufferUtils; @@ -522,6 +523,7 @@ public class Utils { * Returns a the contents of a URL as a string. * @param url the remote URL * @return the contents as a string, or null if any error occurred + * @author Roland Illig (http://stackoverflow.com/a/4308662) */ public static String readDataFromUrl(URL url) throws IOException { // open connection @@ -556,10 +558,9 @@ public class Utils { /** * Returns a JSON object from a URL. * @param url the remote URL - * @return the JSON object - * @author Roland Illig (http://stackoverflow.com/a/4308662) + * @return the JSON object, or null if an error occurred */ - public static JSONObject readJsonFromUrl(URL url) throws IOException { + public static JSONObject readJsonObjectFromUrl(URL url) throws IOException { String s = Utils.readDataFromUrl(url); JSONObject json = null; if (s != null) { @@ -572,6 +573,24 @@ public class Utils { return json; } + /** + * Returns a JSON array from a URL. + * @param url the remote URL + * @return the JSON array, or null if an error occurred + */ + public static JSONArray readJsonArrayFromUrl(URL url) throws IOException { + String s = Utils.readDataFromUrl(url); + JSONArray json = null; + if (s != null) { + try { + json = new JSONArray(s); + } catch (JSONException e) { + ErrorHandler.error("Failed to create JSON array.", e, true); + } + } + return json; + } + /** * Converts an input stream to a string. * @param is the input stream diff --git a/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java b/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java index 94291e68..c696e800 100644 --- a/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java @@ -68,7 +68,7 @@ public class BloodcatServer extends DownloadServer { try { // read JSON String search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), rankedOnly ? "0" : "", page); - JSONObject json = Utils.readJsonFromUrl(new URL(search)); + JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); if (json == null) { this.totalResults = -1; return null; diff --git a/src/itdelatrisu/opsu/downloads/servers/HexideServer.java b/src/itdelatrisu/opsu/downloads/servers/HexideServer.java new file mode 100644 index 00000000..1d31289f --- /dev/null +++ b/src/itdelatrisu/opsu/downloads/servers/HexideServer.java @@ -0,0 +1,139 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu.downloads.servers; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.downloads.DownloadNode; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Download server: https://osu.hexide.com/ + */ +public class HexideServer extends DownloadServer { + /** Server name. */ + private static final String SERVER_NAME = "Hexide"; + + /** Formatted download URL: {@code beatmapSetID,beatmapSetID} */ + private static final String DOWNLOAD_URL = "https://osu.hexide.com/beatmaps/%d/download/%d.osz"; + + /** API fields. */ + private static final String API_FIELDS = "maps.ranked_id;maps.title;maps.date;metadata.m_title;metadata.m_artist;metadata.m_creator"; + + /** Maximum beatmaps displayed per page. */ + private static final int PAGE_LIMIT = 20; + + /** Formatted home URL: {@code page} */ + private static final String HOME_URL = "https://osu.hexide.com/search/" + API_FIELDS + "/maps.size.gt.0/order.date.desc/limit.%d." + (PAGE_LIMIT + 1); + + /** Formatted search URL: {@code query,page} */ + private static final String SEARCH_URL = "https://osu.hexide.com/search/" + API_FIELDS + "/maps.title.like.%s/order.date.desc/limit.%d." + (PAGE_LIMIT + 1); + + /** Total result count from the last query. */ + private int totalResults = -1; + + /** Constructor. */ + public HexideServer() {} + + @Override + public String getName() { return SERVER_NAME; } + + @Override + public String getDownloadURL(int beatmapSetID) { + return String.format(DOWNLOAD_URL, beatmapSetID, beatmapSetID); + } + + @Override + public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException { + DownloadNode[] nodes = null; + try { + // read JSON + int resultIndex = (page - 1) * PAGE_LIMIT; + String search; + if (query.isEmpty()) + search = String.format(HOME_URL, resultIndex); + else + search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), resultIndex); + URL searchURL = new URL(search); + JSONArray arr = null; + try { + arr = Utils.readJsonArrayFromUrl(searchURL); + } catch (IOException e1) { + // a valid search with no results still throws an exception (?) + this.totalResults = 0; + return new DownloadNode[0]; + } + if (arr == null) { + this.totalResults = -1; + return null; + } + + // parse result list + nodes = new DownloadNode[Math.min(arr.length(), PAGE_LIMIT)]; + for (int i = 0; i < nodes.length && i < PAGE_LIMIT; i++) { + JSONObject item = arr.getJSONObject(i); + String title, artist, creator; + if (item.has("versions")) { + JSONArray versions = item.getJSONArray("versions"); + JSONObject version = versions.getJSONObject(0); + title = version.getString("m_title"); + artist = version.getString("m_artist"); + creator = version.getString("m_creator"); + } else { // "versions" is sometimes missing (?) + String str = item.getString("title"); + int index = str.indexOf(" - "); + if (index > -1) { + title = str.substring(0, index); + artist = str.substring(index + 3); + creator = "?"; + } else { // should never happen... + title = str; + artist = creator = "?"; + } + } + nodes[i] = new DownloadNode( + item.getInt("ranked_id"), item.getString("date"), + title, null, artist, null, creator + ); + } + + // store total result count + // NOTE: The API doesn't provide a result count without retrieving + // all results at once; this approach just gets pagination correct. + this.totalResults = arr.length() + resultIndex; + } catch (MalformedURLException | UnsupportedEncodingException e) { + ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); + } + return nodes; + } + + @Override + public int minQueryLength() { return 0; } + + @Override + public int totalResults() { return totalResults; } +} diff --git a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java index 7c2e54e4..d7179664 100644 --- a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java @@ -91,7 +91,7 @@ public class OsuMirrorServer extends DownloadServer { isSearch = true; search = String.format(SEARCH_URL, page, URLEncoder.encode(query, "UTF-8")); } - JSONObject json = Utils.readJsonFromUrl(new URL(search)); + JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); if (json == null || json.getInt("code") != 0) { this.totalResults = -1; return null; diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 5625116e..9edafa4d 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -36,6 +36,7 @@ import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadNode; import itdelatrisu.opsu.downloads.servers.BloodcatServer; import itdelatrisu.opsu.downloads.servers.DownloadServer; +import itdelatrisu.opsu.downloads.servers.HexideServer; import itdelatrisu.opsu.downloads.servers.OsuMirrorServer; import java.io.File; @@ -73,7 +74,7 @@ public class DownloadsMenu extends BasicGameState { private static final int MIN_REQUEST_INTERVAL = 300; /** Available beatmap download servers. */ - private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer() }; + private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() }; /** The beatmap download server index. */ private int serverIndex = 0; @@ -446,6 +447,14 @@ public class DownloadsMenu extends BasicGameState { queryThread.start(); } } + + // tooltips + if (resetButton.contains(mouseX, mouseY)) + UI.updateTooltip(delta, "Reset the current search.", false); + else if (rankedButton.contains(mouseX, mouseY)) + UI.updateTooltip(delta, "Toggle the display of unranked maps.\nSome download servers may not support this option.", true); + else if (serverButton.contains(mouseX, mouseY)) + UI.updateTooltip(delta, "Select a download server.", false); } @Override