From cab207e27587efd768fce7f4391bddf2739291d9 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 7 May 2015 23:58:04 -0400 Subject: [PATCH] Added osu!Mirror download server. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Utils.java | 21 +++ .../opsu/downloads/DownloadNode.java | 35 +++-- .../{ => servers}/BloodcatServer.java | 36 ++--- .../{ => servers}/DownloadServer.java | 20 ++- .../downloads/servers/OsuMirrorServer.java | 129 ++++++++++++++++++ .../opsu/states/DownloadsMenu.java | 96 +++++++++---- 6 files changed, 268 insertions(+), 69 deletions(-) rename src/itdelatrisu/opsu/downloads/{ => servers}/BloodcatServer.java (81%) rename src/itdelatrisu/opsu/downloads/{ => servers}/DownloadServer.java (80%) create mode 100644 src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 379ff08b..bdf9b6be 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -51,6 +51,8 @@ import java.util.Scanner; import javax.imageio.ImageIO; +import org.json.JSONException; +import org.json.JSONObject; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; @@ -554,6 +556,25 @@ 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) + */ + public static JSONObject readJsonFromUrl(URL url) throws IOException { + String s = Utils.readDataFromUrl(url); + JSONObject json = null; + if (s != null) { + try { + json = new JSONObject(s); + } catch (JSONException e) { + ErrorHandler.error("Failed to create JSON object.", e, true); + } + } + return json; + } + /** * Converts an input stream to a string. * @param is the input stream diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 0040aa2e..f13668f5 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -26,6 +26,7 @@ import itdelatrisu.opsu.UI; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.downloads.Download.DownloadListener; import itdelatrisu.opsu.downloads.Download.Status; +import itdelatrisu.opsu.downloads.servers.DownloadServer; import java.io.File; @@ -239,22 +240,26 @@ public class DownloadNode { * @see #getDownload() */ public void createDownload(DownloadServer server) { - if (download == null) { - String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID); - String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title); - this.download = new Download(server.getURL(beatmapSetID), path, rename); - download.setListener(new DownloadListener() { - @Override - public void completed() { - UI.sendBarNotification(String.format("Download complete: %s", getTitle())); - } + if (download != null) + return; - @Override - public void error() { - UI.sendBarNotification("Download failed due to a connection error."); - } - }); - } + String url = server.getDownloadURL(beatmapSetID); + if (url == null) + return; + String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID); + String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title); + this.download = new Download(url, path, rename); + download.setListener(new DownloadListener() { + @Override + public void completed() { + UI.sendBarNotification(String.format("Download complete: %s", getTitle())); + } + + @Override + public void error() { + UI.sendBarNotification("Download failed due to a connection error."); + } + }); } /** diff --git a/src/itdelatrisu/opsu/downloads/BloodcatServer.java b/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java similarity index 81% rename from src/itdelatrisu/opsu/downloads/BloodcatServer.java rename to src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java index 111cc77a..6bf0c4bd 100644 --- a/src/itdelatrisu/opsu/downloads/BloodcatServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/BloodcatServer.java @@ -16,10 +16,11 @@ * along with opsu!. If not, see . */ -package itdelatrisu.opsu.downloads; +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; @@ -28,13 +29,15 @@ import java.net.URL; import java.net.URLEncoder; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; /** * Download server: http://bloodcat.com/osu/ */ public class BloodcatServer extends DownloadServer { + /** Server name. */ + private static final String SERVER_NAME = "Bloodcat"; + /** Formatted download URL: {@code beatmapSetID} */ private static final String DOWNLOAD_URL = "http://bloodcat.com/osu/s/%d"; @@ -48,7 +51,10 @@ public class BloodcatServer extends DownloadServer { public BloodcatServer() {} @Override - public String getURL(int beatmapSetID) { + public String getName() { return SERVER_NAME; } + + @Override + public String getDownloadURL(int beatmapSetID) { return String.format(DOWNLOAD_URL, beatmapSetID); } @@ -58,7 +64,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 = readJsonFromUrl(new URL(search)); + JSONObject json = Utils.readJsonFromUrl(new URL(search)); if (json == null) { this.totalResults = -1; return null; @@ -86,24 +92,8 @@ public class BloodcatServer extends DownloadServer { } @Override - public int totalResults() { return totalResults; } + public int minQueryLength() { return 0; } - /** - * Returns a JSON object from a URL. - * @param url the remote URL - * @return the JSON object - * @author Roland Illig (http://stackoverflow.com/a/4308662) - */ - private static JSONObject readJsonFromUrl(URL url) throws IOException { - String s = Utils.readDataFromUrl(url); - JSONObject json = null; - if (s != null) { - try { - json = new JSONObject(s); - } catch (JSONException e) { - ErrorHandler.error("Failed to create JSON object.", e, true); - } - } - return json; - } + @Override + public int totalResults() { return totalResults; } } diff --git a/src/itdelatrisu/opsu/downloads/DownloadServer.java b/src/itdelatrisu/opsu/downloads/servers/DownloadServer.java similarity index 80% rename from src/itdelatrisu/opsu/downloads/DownloadServer.java rename to src/itdelatrisu/opsu/downloads/servers/DownloadServer.java index b3840364..24cca1f5 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/DownloadServer.java @@ -16,7 +16,9 @@ * along with opsu!. If not, see . */ -package itdelatrisu.opsu.downloads; +package itdelatrisu.opsu.downloads.servers; + +import itdelatrisu.opsu.downloads.DownloadNode; import java.io.IOException; @@ -27,12 +29,18 @@ public abstract class DownloadServer { /** Track preview URL. */ private static final String PREVIEW_URL = "http://b.ppy.sh/preview/%d.mp3"; + /** + * Returns the name of the download server. + * @return the server name + */ + public abstract String getName(); + /** * Returns a web address to download the given beatmap. * @param beatmapSetID the beatmap set ID - * @return the URL string + * @return the URL string, or null if the address could not be determined */ - public abstract String getURL(int beatmapSetID); + public abstract String getDownloadURL(int beatmapSetID); /** * Returns a list of results for a given search query, or null if the @@ -45,6 +53,12 @@ public abstract class DownloadServer { */ public abstract DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException; + /** + * Returns the minimum allowable length of a search query. + * @return the minimum length, or 0 if none + */ + public abstract int minQueryLength(); + /** * Returns the total number of results for the last search query. * This will differ from the the size of the array returned by diff --git a/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java new file mode 100644 index 00000000..b4cb5f3e --- /dev/null +++ b/src/itdelatrisu/opsu/downloads/servers/OsuMirrorServer.java @@ -0,0 +1,129 @@ +/* + * 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 java.util.HashMap; + +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Download server: http://loli.al/ + */ +public class OsuMirrorServer extends DownloadServer { + /** Server name. */ + private static final String SERVER_NAME = "osu!Mirror"; + + /** Formatted download URL: {@code beatmapSetID} */ + private static final String DOWNLOAD_URL = "http://loli.al/d/%d/"; + + /** Formatted search URL: {@code page,query} */ + private static final String SEARCH_URL = "http://loli.al/mirror/search/%d.json?keyword=%s"; + + /** Formatted home URL: {@code page} */ + private static final String HOME_URL = "http://loli.al/mirror/home/%d.json"; + + /** Minimum allowable length of a search query. */ + private static final int MIN_QUERY_LENGTH = 3; + + /** Total result count from the last query. */ + private int totalResults = -1; + + /** Max server download ID seen (for approximating total pages). */ + private int maxServerID = 0; + + /** Lookup table from beatmap set ID -> server download ID. */ + private HashMap idTable = new HashMap(); + + /** Constructor. */ + public OsuMirrorServer() {} + + @Override + public String getName() { return SERVER_NAME; } + + @Override + public String getDownloadURL(int beatmapSetID) { + return (idTable.containsKey(beatmapSetID)) ? String.format(DOWNLOAD_URL, idTable.get(beatmapSetID)) : null; + } + + @Override + public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException { + // NOTE: ignores 'rankedOnly' flag. + DownloadNode[] nodes = null; + try { + // read JSON + String search; + boolean isSearch; + if (query.isEmpty()) { + isSearch = false; + search = String.format(HOME_URL, page); + } else { + isSearch = true; + search = String.format(SEARCH_URL, page, URLEncoder.encode(query, "UTF-8")); + } + JSONObject json = Utils.readJsonFromUrl(new URL(search)); + if (json == null || json.getInt("code") != 0) { + this.totalResults = -1; + return null; + } + + // parse result list + JSONArray arr = json.getJSONArray("maplist"); + nodes = new DownloadNode[arr.length()]; + for (int i = 0; i < nodes.length; i++) { + JSONObject item = arr.getJSONObject(i); + int beatmapSetID = item.getInt("OSUSetid"); + int serverID = item.getInt("id"); + nodes[i] = new DownloadNode( + beatmapSetID, item.getString("ModifyDate"), + item.getString("Title"), null, + item.getString("Artist"), null, + item.getString("Mapper") + ); + idTable.put(beatmapSetID, serverID); + if (serverID > maxServerID) + maxServerID = serverID; + } + + // store total result count + if (isSearch) + this.totalResults = json.getInt("totalRows"); + else + this.totalResults = maxServerID; + } 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 MIN_QUERY_LENGTH; } + + @Override + public int totalResults() { return totalResults; } +} diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 52d46087..8172f9cb 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -31,11 +31,12 @@ import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; -import itdelatrisu.opsu.downloads.BloodcatServer; import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadNode; -import itdelatrisu.opsu.downloads.DownloadServer; +import itdelatrisu.opsu.downloads.servers.BloodcatServer; +import itdelatrisu.opsu.downloads.servers.DownloadServer; +import itdelatrisu.opsu.downloads.servers.OsuMirrorServer; import java.io.File; import java.io.IOException; @@ -71,8 +72,11 @@ public class DownloadsMenu extends BasicGameState { /** Minimum time, in milliseconds, that must elapse between queries. */ private static final int MIN_REQUEST_INTERVAL = 300; - /** The beatmap download server. */ - private DownloadServer server = new BloodcatServer(); + /** Available beatmap download servers. */ + private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer() }; + + /** The beatmap download server index. */ + private int serverIndex = 0; /** The current list of search results. */ private DownloadNode[] resultList; @@ -138,7 +142,7 @@ public class DownloadsMenu extends BasicGameState { private MenuButton prevPage, nextPage; /** Buttons. */ - private MenuButton clearButton, importButton, resetButton, rankedButton; + private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton; /** Beatmap importing thread. */ private Thread importThread; @@ -170,11 +174,11 @@ public class DownloadsMenu extends BasicGameState { int height = container.getHeight(); float baseX = width * 0.024f; float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight(); - float searchWidth = width * 0.35f; + float searchWidth = width * 0.3f; // search searchTimer = SEARCH_DELAY; - searchResultString = "Type to search!"; + searchResultString = "Loading data from server..."; search = new TextField( container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY, (int) searchWidth, Utils.FONT_MEDIUM.getLineHeight() @@ -200,8 +204,10 @@ public class DownloadsMenu extends BasicGameState { // buttons float buttonMarginX = width * 0.004f; float buttonHeight = height * 0.038f; - float topButtonWidth = width * 0.14f; - float lowerButtonWidth = width * 0.12f; + float resetWidth = width * 0.085f; + float rankedWidth = width * 0.15f; + float serverWidth = width * 0.12f; + float lowerWidth = width * 0.12f; float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f; float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f; Image button = GameImage.MENU_BUTTON_MID.getImage(); @@ -209,25 +215,33 @@ public class DownloadsMenu extends BasicGameState { Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage(); buttonL = buttonL.getScaledCopy(buttonHeight / buttonL.getHeight()); buttonR = buttonR.getScaledCopy(buttonHeight / buttonR.getHeight()); - Image topButton = button.getScaledCopy((int) topButtonWidth - buttonL.getWidth() - buttonR.getWidth(), (int) buttonHeight); - Image lowerButton = button.getScaledCopy((int) lowerButtonWidth - buttonL.getWidth() - buttonR.getWidth(), (int) buttonHeight); - float fullTopButtonWidth = topButton.getWidth() + buttonL.getWidth() + buttonR.getWidth(); - float fullLowerButtonWidth = lowerButton.getWidth() + buttonL.getWidth() + buttonR.getWidth(); - clearButton = new MenuButton(lowerButton, buttonL, buttonR, - width * 0.75f + buttonMarginX + fullLowerButtonWidth / 2f, lowerButtonY); - importButton = new MenuButton(lowerButton, buttonL, buttonR, - width - buttonMarginX - fullLowerButtonWidth / 2f, lowerButtonY); - resetButton = new MenuButton(topButton, buttonL, buttonR, - baseX + searchWidth + buttonMarginX + fullTopButtonWidth / 2f, topButtonY); - rankedButton = new MenuButton(topButton, buttonL, buttonR, - baseX + searchWidth + buttonMarginX * 2f + fullTopButtonWidth * 3 / 2f, topButtonY); + int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth(); + Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight); + Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight); + Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight); + Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight); + float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth; + float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth; + float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth; + float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth; + clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR, + width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY); + importButton = new MenuButton(lowerButtonImage, buttonL, buttonR, + width - buttonMarginX - lowerButtonWidth / 2f, lowerButtonY); + resetButton = new MenuButton(resetButtonImage, buttonL, buttonR, + baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY); + rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR, + baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY); + serverButton = new MenuButton(serverButtonImage, buttonL, buttonR, + baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth + serverButtonWidth / 2f, topButtonY); clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white); importButton.setText("Import All", Utils.FONT_MEDIUM, Color.white); - resetButton.setText("Reset Search", Utils.FONT_MEDIUM, Color.white); + resetButton.setText("Reset", Utils.FONT_MEDIUM, Color.white); clearButton.setHoverFade(); importButton.setHoverFade(); resetButton.setHoverFade(); rankedButton.setHoverFade(); + serverButton.setHoverFade(); } @Override @@ -317,6 +331,8 @@ public class DownloadsMenu extends BasicGameState { resetButton.draw(Color.red); rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white); rankedButton.draw(Color.magenta); + serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white); + serverButton.draw(Color.blue); // importing beatmaps if (importThread != null) { @@ -348,6 +364,7 @@ public class DownloadsMenu extends BasicGameState { importButton.hoverUpdate(delta, mouseX, mouseY); resetButton.hoverUpdate(delta, mouseX, mouseY); rankedButton.hoverUpdate(delta, mouseX, mouseY); + serverButton.hoverUpdate(delta, mouseX, mouseY); // focus timer if (focusResult != -1 && focusTimer < FOCUS_DELAY) @@ -361,7 +378,9 @@ public class DownloadsMenu extends BasicGameState { searchTimerReset = false; final String query = search.getText().trim().toLowerCase(); - if (lastQuery == null || !query.equals(lastQuery)) { + final DownloadServer server = SERVERS[serverIndex]; + if ((lastQuery == null || !query.equals(lastQuery)) && + (query.length() == 0 || query.length() >= server.minQueryLength())) { lastQuery = query; lastQueryDir = pageDir; @@ -409,7 +428,7 @@ public class DownloadsMenu extends BasicGameState { else { if (query.isEmpty()) searchResultString = "Type to search!"; - else if (totalResults == 0) + else if (totalResults == 0 || resultList.length == 0) searchResultString = "No results found."; else searchResultString = String.format("%d result%s found!", @@ -481,7 +500,7 @@ public class DownloadsMenu extends BasicGameState { } else { // play preview try { - final URL url = new URL(server.getPreviewURL(node.getID())); + final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID())); MusicController.pause(); new Thread() { @Override @@ -525,9 +544,13 @@ public class DownloadsMenu extends BasicGameState { } else { // start download if (!DownloadList.get().contains(node.getID())) { - DownloadList.get().addNode(node); - node.createDownload(server); - node.getDownload().start(); + node.createDownload(SERVERS[serverIndex]); + if (node.getDownload() == null) + UI.sendBarNotification("The download could not be started."); + else { + DownloadList.get().addNode(node); + node.getDownload().start(); + } } } } else { @@ -624,6 +647,22 @@ public class DownloadsMenu extends BasicGameState { resetSearchTimer(); return; } + if (serverButton.contains(x, y)) { + SoundController.playSound(SoundEffect.MENUCLICK); + resultList = null; + startResult = 0; + focusResult = -1; + totalResults = 0; + page = 0; + pageResultTotal = 1; + pageDir = Page.RESET; + searchResultString = "Loading data from server..."; + serverIndex = (serverIndex + 1) % SERVERS.length; + lastQuery = null; + pageDir = Page.RESET; + resetSearchTimer(); + return; + } // downloads if (!DownloadList.get().isEmpty() && DownloadNode.downloadAreaContains(x, y)) { @@ -755,6 +794,7 @@ public class DownloadsMenu extends BasicGameState { importButton.resetHover(); resetButton.resetHover(); rankedButton.resetHover(); + serverButton.resetHover(); focusResult = -1; startResult = 0; startDownloadIndex = 0;