Merge remote-tracking branch 'org/master' into KinecticScrolling

Conflicts:
	src/itdelatrisu/opsu/ScoreData.java
	src/itdelatrisu/opsu/downloads/DownloadNode.java
	src/itdelatrisu/opsu/states/DownloadsMenu.java
	src/itdelatrisu/opsu/states/SongMenu.java
This commit is contained in:
fd
2015-09-14 19:32:18 -04:00
102 changed files with 6168 additions and 1857 deletions

View File

@@ -46,6 +46,9 @@ public class Download {
/** Read timeout, in ms. */
public static final int READ_TIMEOUT = 10000;
/** Maximum number of HTTP/HTTPS redirects to follow. */
public static final int MAX_REDIRECTS = 3;
/** Time between download speed and ETA updates, in ms. */
private static final int UPDATE_INTERVAL = 1000;
@@ -58,7 +61,7 @@ public class Download {
ERROR ("Error");
/** The status name. */
private String name;
private final String name;
/**
* Constructor.
@@ -172,13 +175,57 @@ public class Download {
new Thread() {
@Override
public void run() {
// open connection, get content length
// open connection
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
URL downloadURL = url;
int redirectCount = 0;
boolean isRedirect = false;
do {
isRedirect = false;
conn = (HttpURLConnection) downloadURL.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
// allow HTTP <--> HTTPS redirects
// http://download.java.net/jdk7u2/docs/technotes/guides/deployment/deployment-guide/upgrade-guide/article-17.html
conn.setInstanceFollowRedirects(false);
conn.setRequestProperty("User-Agent", "Mozilla/5.0...");
// check for redirect
int status = conn.getResponseCode();
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM ||
status == HttpURLConnection.HTTP_SEE_OTHER || status == HttpURLConnection.HTTP_USE_PROXY) {
URL base = conn.getURL();
String location = conn.getHeaderField("Location");
URL target = null;
if (location != null)
target = new URL(base, location);
conn.disconnect();
// check for problems
String error = null;
if (location == null)
error = String.format("Download for URL '%s' is attempting to redirect without a 'location' header.", base.toString());
else if (!target.getProtocol().equals("http") && !target.getProtocol().equals("https"))
error = String.format("Download for URL '%s' is attempting to redirect to a non-HTTP/HTTPS protocol '%s'.", base.toString(), target.getProtocol());
else if (redirectCount > MAX_REDIRECTS)
error = String.format("Download for URL '%s' is attempting too many redirects (over %d).", base.toString(), MAX_REDIRECTS);
if (error != null) {
ErrorHandler.error(error, null, false);
throw new IOException();
}
// follow redirect
downloadURL = target;
redirectCount++;
isRedirect = true;
}
} while (isRedirect);
// store content length
contentLength = conn.getContentLength();
} catch (IOException e) {
status = Status.ERROR;
@@ -198,9 +245,18 @@ public class Download {
fos = fileOutputStream;
status = Status.DOWNLOADING;
updateReadSoFar();
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
long bytesRead = fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
if (status == Status.DOWNLOADING) { // not interrupted
// TODO: if connection is lost before a download finishes, it's still marked as "complete"
// check if the entire file was received
if (bytesRead < contentLength) {
status = Status.ERROR;
Log.warn(String.format("Download '%s' failed: %d bytes expected, %d bytes received.", url.toString(), contentLength, bytesRead));
if (listener != null)
listener.error();
return;
}
// mark download as complete
status = Status.COMPLETE;
rbc.close();
fos.close();
@@ -273,7 +329,7 @@ public class Download {
public long readSoFar() {
switch (status) {
case COMPLETE:
return contentLength;
return (rbc != null) ? rbc.getReadSoFar() : contentLength;
case DOWNLOADING:
if (rbc != null)
return rbc.getReadSoFar();

View File

@@ -75,7 +75,7 @@ public class DownloadList {
}
/**
* Returns the size of the doownloads list.
* Returns the size of the downloads list.
*/
public int size() { return nodes.size(); }

View File

@@ -26,6 +26,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.downloads.Download.DownloadListener;
import itdelatrisu.opsu.downloads.Download.Status;
import itdelatrisu.opsu.downloads.servers.DownloadServer;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import java.io.File;
@@ -42,19 +44,19 @@ public class DownloadNode {
private Download download;
/** Beatmap set ID. */
private int beatmapSetID;
private final int beatmapSetID;
/** Last updated date string. */
private String date;
private final String date;
/** Song title. */
private String title, titleUnicode;
private final String title, titleUnicode;
/** Song artist. */
private String artist, artistUnicode;
private final String artist, artistUnicode;
/** Beatmap creator. */
private String creator;
private final String creator;
/** Button drawing values. */
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
@@ -68,12 +70,6 @@ public class DownloadNode {
/** Container width. */
private static int containerWidth;
/** Button background colors. */
public static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_HOVER = new Color(0, 0, 0, 0.5f),
BG_FOCUS = new Color(0, 0, 0, 0.75f);
/**
* Initializes the base coordinates for drawing.
* @param width the container width
@@ -86,16 +82,16 @@ public class DownloadNode {
buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f;
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
buttonHeight = Fonts.MEDIUM.getLineHeight() * 2.1f;
buttonOffset = buttonHeight * 1.1f;
// download info
infoBaseX = width * 0.75f;
infoBaseY = height * 0.07f + Utils.FONT_LARGE.getLineHeight() * 2f;
infoBaseY = height * 0.07f + Fonts.LARGE.getLineHeight() * 2f;
infoWidth = width * 0.25f;
infoHeight = Utils.FONT_DEFAULT.getLineHeight() * 2.4f;
infoHeight = Fonts.DEFAULT.getLineHeight() * 2.4f;
float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight();
float searchY = (height * 0.05f) + Fonts.LARGE.getLineHeight();
float buttonHeight = height * 0.038f;
maxResultsShown = (int) ((height - buttonBaseY - searchY) / buttonOffset);
maxDownloadsShown = (int) ((height - infoBaseY - searchY - buttonHeight) / infoHeight);
@@ -228,10 +224,9 @@ public class DownloadNode {
* @param total the total number of buttons
*/
public static void drawResultScrollbar(Graphics g, float position, float total) {
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset,
buttonBaseX, buttonBaseY,
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset, buttonBaseX, buttonBaseY,
buttonWidth * 1.01f, (maxResultsShown-1) * buttonOffset + buttonHeight,
BG_NORMAL, Color.white, true);
Colors.BLACK_BG_NORMAL, Color.white, true);
}
/**
@@ -242,11 +237,18 @@ public class DownloadNode {
*/
public static void drawDownloadScrollbar(Graphics g, float index, float total) {
UI.drawScrollbar(g, index, total, maxDownloadsShown * infoHeight, infoBaseX, infoBaseY,
infoWidth, maxDownloadsShown * infoHeight, BG_NORMAL, Color.white, true);
infoWidth, maxDownloadsShown * infoHeight, Colors.BLACK_BG_NORMAL, Color.white, true);
}
/**
* Constructor.
* @param beatmapSetID the beatmap set ID
* @param date the last modified date string
* @param title the song title
* @param titleUnicode the Unicode song title (or {@code null} if none)
* @param artist the song artist
* @param artistUnicode the Unicode song artist (or {@code null} if none)
* @param creator the beatmap creator
*/
public DownloadNode(int beatmapSetID, String date, String title,
String titleUnicode, String artist, String artistUnicode, String creator) {
@@ -273,7 +275,7 @@ public class DownloadNode {
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 download = new Download(url, path, rename);
download.setListener(new DownloadListener() {
@Override
public void completed() {
@@ -285,8 +287,9 @@ public class DownloadNode {
UI.sendBarNotification("Download failed due to a connection error.");
}
});
this.download = download;
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_LARGE, getTitle(), null);
Fonts.loadGlyphs(Fonts.LARGE, getTitle());
}
/**
@@ -348,12 +351,12 @@ public class DownloadNode {
Download dl = DownloadList.get().getDownload(beatmapSetID);
// rectangle outline
g.setColor((focus) ? BG_FOCUS : (hover) ? BG_HOVER : BG_NORMAL);
g.setColor((focus) ? Colors.BLACK_BG_FOCUS : (hover) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// map is already loaded
if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
g.setColor(Utils.COLOR_BLUE_BUTTON);
g.setColor(Colors.BLUE_BUTTON);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
}
@@ -361,7 +364,7 @@ public class DownloadNode {
if (dl != null) {
float progress = dl.getProgress();
if (progress > 0f) {
g.setColor(Utils.COLOR_GREEN);
g.setColor(Colors.GREEN);
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
}
}
@@ -373,21 +376,22 @@ public class DownloadNode {
// text
// TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist());
if (Options.useUnicodeMetadata()) { // load glyphs
Fonts.loadGlyphs(Fonts.BOLD, getTitle());
Fonts.loadGlyphs(Fonts.BOLD, getArtist());
}
// TODO can't set clip again or else old clip will be cleared
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight());
Utils.FONT_BOLD.drawString(
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Fonts.DEFAULT.getWidth(creator)), Fonts.BOLD.getLineHeight());
Fonts.BOLD.drawString(
textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
//g.clearClip();
Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
Fonts.DEFAULT.drawString(
textX, y + marginY + Fonts.BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white);
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
Fonts.DEFAULT.drawString(
edgeX - Fonts.DEFAULT.getWidth(creator), y + marginY,
creator, Color.white);
}
@@ -399,6 +403,7 @@ public class DownloadNode {
* @param hover true if the mouse is hovering over this button
*/
public void drawDownload(Graphics g, float position, int id, boolean hover) {
Download download = this.download; // in case clearDownload() is called asynchronously
if (download == null) {
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
return;
@@ -410,7 +415,7 @@ public class DownloadNode {
float marginY = infoHeight * 0.04f;
// rectangle outline
g.setColor((id % 2 == 0) ? BG_HOVER : BG_NORMAL);
g.setColor((id % 2 == 0) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
// text
@@ -428,8 +433,8 @@ public class DownloadNode {
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
}
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);
Fonts.BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Fonts.DEFAULT.drawString(textX, y + marginY + Fonts.BOLD.getLineHeight(), info, Color.white);
// 'x' button
if (hover) {

View File

@@ -28,7 +28,7 @@ import java.nio.channels.ReadableByteChannel;
*/
public class ReadableByteChannelWrapper implements ReadableByteChannel {
/** The wrapped ReadableByteChannel. */
private ReadableByteChannel rbc;
private final ReadableByteChannel rbc;
/** The number of bytes read. */
private long bytesRead;

View File

@@ -77,7 +77,7 @@ public class Updater {
UPDATE_FINAL ("Update queued.");
/** The status description. */
private String description;
private final String description;
/**
* Constructor.
@@ -194,9 +194,10 @@ public class Updater {
/**
* Checks the program version against the version file on the update server.
* @throws IOException if an I/O exception occurs
*/
public void checkForUpdates() throws IOException {
if (status != Status.INITIAL || System.getProperty("XDG") != null)
if (status != Status.INITIAL || Options.USE_XDG)
return;
status = Status.CHECKING;

View File

@@ -75,4 +75,7 @@ public abstract class DownloadServer {
public String getPreviewURL(int beatmapSetID) {
return String.format(PREVIEW_URL, beatmapSetID);
}
@Override
public String toString() { return getName(); }
}

View File

@@ -0,0 +1,202 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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.ArrayList;
import java.util.List;
/**
* Download server: http://osu.mengsky.net/
*/
public class MengSkyServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "MengSky";
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://osu.mengsky.net/d.php?id=%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "http://osu.mengsky.net/index.php?search_keywords=%s";
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "http://osu.mengsky.net/index.php?next=1&page=%d";
/** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 20;
/** Total result count from the last query. */
private int totalResults = -1;
/** Constructor. */
public MengSkyServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read HTML
String search;
boolean isSearch;
if (query.isEmpty()) {
isSearch = false;
search = String.format(HOME_URL, page - 1);
} else {
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
}
String html = Utils.readDataFromUrl(new URL(search));
if (html == null) {
this.totalResults = -1;
return null;
}
// parse results
// NOTE: Maybe an HTML parser would be better for this...
// FORMAT:
// <div class="beatmap" style="{{...}}">
// <div class="preview" style="background-image:url(http://b.ppy.sh/thumb/{{id}}l.jpg)"></div>
// <div class="name"> <a href="">{{artist}} - {{title}}</a> </div>
// <div class="douban_details">
// <span>Creator:</span> {{creator}}<br>
// <span>MaxBpm:</span> {{bpm}}<br>
// <span>Title:</span> {{titleUnicode}}<br>
// <span>Artist:</span> {{artistUnicode}}<br>
// <span>Status:</span> <font color={{"#00CD00" || "#EE0000"}}>{{"Ranked?" || "Unranked"}}</font><br>
// </div>
// <div class="details"> <a href=""></a> <br>
// <span>Fork:</span> bloodcat<br>
// <span>UpdateTime:</span> {{yyyy}}/{{mm}}/{{dd}} {{hh}}:{{mm}}:{{ss}}<br>
// <span>Mode:</span> <img id="{{'s' || 'c' || ...}}" src="/img/{{'s' || 'c' || ...}}.png"> {{...}}
// </div>
// <div class="download">
// <a href="https://osu.ppy.sh/s/{{id}}" class=" btn" target="_blank">Osu.ppy</a>
// </div>
// <div class="download">
// <a href="http://osu.mengsky.net/d.php?id={{id}}" class=" btn" target="_blank">DownLoad</a>
// </div>
// </div>
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String
START_TAG = "<div class=\"beatmap\"", NAME_TAG = "<div class=\"name\"> <a href=\"\">",
CREATOR_TAG = "<span>Creator:</span> ", TITLE_TAG = "<span>Title:</span> ", ARTIST_TAG = "<span>Artist:</span> ",
TIMESTAMP_TAG = "<span>UpdateTime:</span> ", DOWNLOAD_TAG = "<div class=\"download\">",
BR_TAG = "<br>", HREF_TAG = "<a href=\"", HREF_TAG_END = "</a>";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
int divCount = 0;
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
divCount++;
int i, j;
// find beatmap
i = html.indexOf(NAME_TAG, index + START_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(HREF_TAG_END, i + 1);
if (j == -1 || j > n) continue;
String beatmap = html.substring(i + NAME_TAG.length(), j);
String[] beatmapTokens = beatmap.split(" - ", 2);
if (beatmapTokens.length < 2)
continue;
String artist = beatmapTokens[0];
String title = beatmapTokens[1];
// find other beatmap details
i = html.indexOf(CREATOR_TAG, j + HREF_TAG_END.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + CREATOR_TAG.length());
if (j == -1 || j > n) continue;
String creator = html.substring(i + CREATOR_TAG.length(), j);
i = html.indexOf(TITLE_TAG, j + BR_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + TITLE_TAG.length());
if (j == -1 || j > n) continue;
String titleUnicode = html.substring(i + TITLE_TAG.length(), j);
i = html.indexOf(ARTIST_TAG, j + BR_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + ARTIST_TAG.length());
if (j == -1 || j > n) continue;
String artistUnicode = html.substring(i + ARTIST_TAG.length(), j);
i = html.indexOf(TIMESTAMP_TAG, j + BR_TAG.length());
if (i == -1 || i >= n) continue;
j = html.indexOf(BR_TAG, i + TIMESTAMP_TAG.length());
if (j == -1 || j > n) continue;
String date = html.substring(i + TIMESTAMP_TAG.length(), j);
// find beatmap ID
i = html.indexOf(DOWNLOAD_TAG, j + BR_TAG.length());
if (i == -1 || i >= n) continue;
i = html.indexOf(HREF_TAG, i + DOWNLOAD_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf('"', i + HREF_TAG.length());
if (j == -1 || j > n) continue;
String downloadURL = html.substring(i + HREF_TAG.length(), j);
String[] downloadTokens = downloadURL.split("(?=\\d*$)", 2);
if (downloadTokens[1].isEmpty()) continue;
int id;
try {
id = Integer.parseInt(downloadTokens[1]);
} catch (NumberFormatException e) {
continue;
}
nodeList.add(new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
if (isSearch)
this.totalResults = nodes.length;
else {
int resultCount = nodes.length + (page - 1) * PAGE_LIMIT;
if (divCount == PAGE_LIMIT)
resultCount++;
this.totalResults = resultCount;
}
} 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 2; }
@Override
public int totalResults() { return totalResults; }
}

View File

@@ -0,0 +1,133 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Download server: http://osu.uu.gl/
*/
public class MnetworkServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "Mnetwork";
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://osu.uu.gl/s/%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "http://osu.uu.gl/d/%s";
/** Total result count from the last query. */
private int totalResults = -1;
/** Beatmap pattern. */
private Pattern BEATMAP_PATTERN = Pattern.compile("^(\\d+) ([^-]+) - (.+)\\.osz$");
/** Constructor. */
public MnetworkServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read HTML
String queryString = (query.isEmpty()) ? "-" : query;
String search = String.format(SEARCH_URL, URLEncoder.encode(queryString, "UTF-8"));
String html = Utils.readDataFromUrl(new URL(search));
if (html == null) {
this.totalResults = -1;
return null;
}
// parse results
// NOTE: Not using a full HTML parser because this is a relatively simple operation.
// FORMAT:
// <div class="tr_title">
// <b><a href='/s/{{id}}'>{{id}} {{artist}} - {{title}}.osz</a></b><br />
// BPM: {{bpm}} <b>|</b> Total Time: {{m}}:{{s}}<br/>
// Genre: {{genre}} <b>|</b> Updated: {{MMM}} {{d}}, {{yyyy}}<br />
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String START_TAG = "<div class=\"tr_title\">", HREF_TAG = "<a href=", HREF_TAG_END = "</a>", UPDATED = "Updated: ";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
int i, j;
// find beatmap
i = html.indexOf(HREF_TAG, index + START_TAG.length());
if (i == -1 || i > n) continue;
i = html.indexOf('>', i + HREF_TAG.length());
if (i == -1 || i >= n) continue;
j = html.indexOf(HREF_TAG_END, i + 1);
if (j == -1 || j > n) continue;
String beatmap = html.substring(i + 1, j).trim();
// find date
i = html.indexOf(UPDATED, j);
if (i == -1 || i >= n) continue;
j = html.indexOf('<', i + UPDATED.length());
if (j == -1 || j > n) continue;
String date = html.substring(i + UPDATED.length(), j).trim();
// parse id, title, and artist
Matcher m = BEATMAP_PATTERN.matcher(beatmap);
if (!m.matches())
continue;
nodeList.add(new DownloadNode(Integer.parseInt(m.group(1)), date, m.group(3), null, m.group(2), null, ""));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
this.totalResults = nodes.length;
} 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; }
}

View File

@@ -39,6 +39,8 @@ import org.json.JSONObject;
/**
* Download server: http://loli.al/
* <p>
* <i>This server went offline in August 2015.</i>
*/
public class OsuMirrorServer extends DownloadServer {
/** Server name. */

View File

@@ -0,0 +1,204 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.json.JSONObject;
/**
* Download server: http://osu.yas-online.net/
*/
public class YaSOnlineServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "YaS Online";
/** Formatted download URL (returns JSON): {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "https://osu.yas-online.net/json.mapdata.php?mapId=%d";
/**
* Formatted download fetch URL: {@code downloadLink}
* (e.g. {@code /fetch/49125122158ef360a66a07bce2d0483596913843-m-10418.osz})
*/
private static final String DOWNLOAD_FETCH_URL = "https://osu.yas-online.net%s";
/** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 25;
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "https://osu.yas-online.net/json.maplist.php?o=%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "https://osu.yas-online.net/json.search.php?searchQuery=%s";
/** Total result count from the last query. */
private int totalResults = -1;
/** Max server download ID seen (for approximating total pages). */
private int maxServerID = 0;
/** Constructor. */
public YaSOnlineServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
try {
// TODO: do this asynchronously (will require lots of changes...)
return getDownloadURLFromMapData(beatmapSetID);
} catch (IOException e) {
return null;
}
}
/**
* Returns the beatmap download URL by downloading its map data.
* <p>
* This is needed because there is no other way to find a beatmap's direct
* download URL.
* @param beatmapSetID the beatmap set ID
* @return the URL string, or null if the address could not be determined
* @throws IOException if any connection error occurred
*/
private String getDownloadURLFromMapData(int beatmapSetID) throws IOException {
try {
// read JSON
String search = String.format(DOWNLOAD_URL, beatmapSetID);
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
JSONObject results;
if (json == null ||
!json.getString("result").equals("success") ||
(results = json.getJSONObject("success")).length() == 0) {
return null;
}
// parse result
Iterator<?> keys = results.keys();
if (!keys.hasNext())
return null;
String key = (String) keys.next();
JSONObject item = results.getJSONObject(key);
String downloadLink = item.getString("downloadLink");
return String.format(DOWNLOAD_FETCH_URL, downloadLink);
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true);
return null;
}
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read JSON
String search;
boolean isSearch;
if (query.isEmpty()) {
isSearch = false;
search = String.format(HOME_URL, (page - 1) * PAGE_LIMIT);
} else {
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
}
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
if (json == null) {
this.totalResults = -1;
return null;
}
JSONObject results;
if (!json.getString("result").equals("success") ||
(results = json.getJSONObject("success")).length() == 0) {
this.totalResults = 0;
return new DownloadNode[0];
}
// parse result list
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
for (Object obj : results.keySet()) {
String key = (String) obj;
JSONObject item = results.getJSONObject(key);
// parse title and artist
String title, artist;
String str = item.getString("map");
int index = str.indexOf(" - ");
if (index > -1) {
title = str.substring(0, index);
artist = str.substring(index + 3);
} else { // should never happen...
title = str;
artist = "?";
}
// only contains date added if part of a beatmap pack
int added = item.getInt("added");
String date = (added == 0) ? "?" : formatDate(added);
// approximate page count
int serverID = item.getInt("id");
if (serverID > maxServerID)
maxServerID = serverID;
nodeList.add(new DownloadNode(item.getInt("mapid"), date, title, null, artist, null, ""));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
if (isSearch)
this.totalResults = nodes.length;
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 3; }
@Override
public int totalResults() { return totalResults; }
/**
* Returns a formatted date string from a raw date.
* @param timestamp the UTC timestamp, in seconds
* @return the formatted date
*/
private String formatDate(int timestamp) {
Date d = new Date(timestamp * 1000L);
DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
return fmt.format(d);
}
}