Implemented an internal beatmap downloader (using Bloodcat).

The downloads menu can be accessed through the button on the right side of the main menu.  The downloader supports searching and concurrent downloads (NOTE: this is limited by the download server!).  Double-click any search result to begin downloading it to the SongPacks directory; cancel the download by hitting the red 'x' in the upper-right corner.  A confirmation will appear if trying to quit opsu! while downloads are running.

New classes:
- Download: represents an individual download from a remote address to a local path, and provides status and progress information; downloads files using Java NIO.
- DownloadNode: holds a Download object as well as additional beatmap fields, and handles drawing.
- DownloadList: manages the current list of downloads.
- DownloadsMenu: game state controller.
- DownloadServer: interface for a beatmap download server.
- BloodcatServer: implements DownloadServer using Bloodcat.
- ReadableByteChannelWrapper: wrapper for ReadableByteChannel that tracks progress.

Added images:
- "downloads" image by @kouyang.
- "search-background" image from "Minimalist Miku" skin (listed in credits).
- "delete" icon by Visual Pharm (https://www.iconfinder.com/icons/27842/) under CC BY-ND 3.0.

Other changes:
- Added up/down/left/right Expand directions to MenuButton class.
- Removed width/height parameters from OsuParser (leftovers).

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han
2015-02-01 02:10:17 -05:00
parent cb5a7d6a4b
commit 70c70fd812
21 changed files with 1701 additions and 22 deletions

View File

@@ -0,0 +1,134 @@
/*
* 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;
import itdelatrisu.opsu.ErrorHandler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
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 implements DownloadServer {
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://bloodcat.com/osu/s/%d";
/** Formatted search URL: {@code query,rankedOnly,page} */
private static final String SEARCH_URL = "http://bloodcat.com/osu/?q=%s&m=b&c=%s&g=&d=0&s=date&o=0&p=%d&mod=json";
/** Total result count from the last query. */
private int totalResults = -1;
/** Constructor. */
public BloodcatServer() {}
@Override
public String getURL(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 JSON
String search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"), rankedOnly ? "0" : "", page);
JSONObject json = readJsonFromUrl(new URL(search));
if (json == null) {
this.totalResults = -1;
return null;
}
// parse result list
JSONArray arr = json.getJSONArray("results");
nodes = new DownloadNode[arr.length()];
for (int i = 0; i < nodes.length; i++) {
JSONObject item = arr.getJSONObject(i);
nodes[i] = new DownloadNode(
item.getInt("id"), item.getString("date"),
item.getString("title"), item.isNull("titleUnicode") ? null : item.getString("titleUnicode"),
item.getString("artist"), item.isNull("artistUnicode") ? null : item.getString("artistUnicode"),
item.getString("creator")
);
}
// store total result count
this.totalResults = json.getInt("resultCount");
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
}
return nodes;
}
@Override
public int totalResults() { return totalResults; }
/**
* 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 {
// open connection
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(Download.CONNECTION_TIMEOUT);
conn.setReadTimeout(Download.READ_TIMEOUT);
conn.setUseCaches(false);
try {
conn.connect();
} catch (SocketTimeoutException e) {
ErrorHandler.error("Connection to server timed out.", e, false);
throw e;
}
if (Thread.interrupted())
return null;
// read JSON
JSONObject json = null;
try (InputStream in = conn.getInputStream()) {
BufferedReader rd = new BufferedReader(new InputStreamReader(in));
StringBuilder sb = new StringBuilder();
int c;
while ((c = rd.read()) != -1)
sb.append((char) c);
json = new JSONObject(sb.toString());
} catch (SocketTimeoutException e) {
ErrorHandler.error("Connection to server timed out.", e, false);
throw e;
} catch (JSONException e1) {
ErrorHandler.error("Failed to create JSON object.", e1, true);
}
return json;
}
}

View File

@@ -0,0 +1,256 @@
/*
* 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;
import itdelatrisu.opsu.ErrorHandler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
/**
* File download.
*/
public class Download {
/** Connection timeout, in ms. */
public static final int CONNECTION_TIMEOUT = 5000;
/** Read timeout, in ms. */
public static final int READ_TIMEOUT = 10000;
/** Download statuses. */
public enum Status {
WAITING ("Waiting"),
DOWNLOADING ("Downloading"),
COMPLETE ("Complete"),
CANCELLED ("Cancelled"),
ERROR ("Error");
/** The status name. */
private String name;
/**
* Constructor.
* @param name the status name.
*/
Status(String name) {
this.name = name;
}
/**
* Returns the status name.
*/
public String getName() { return name; }
}
/** The local path. */
private String localPath;
/** The local path to rename the file to when finished. */
private String rename;
/** The download URL. */
private URL url;
/** The readable byte channel. */
private ReadableByteChannelWrapper rbc;
/** The file output stream. */
private FileOutputStream fos;
/** The size of the download. */
private int contentLength = -1;
/** The download status. */
private Status status = Status.WAITING;
/**
* Constructor.
* @param remoteURL the download URL
* @param localPath the path to save the download
*/
public Download(String remoteURL, String localPath) {
this(remoteURL, localPath, null);
}
/**
* Constructor.
* @param remoteURL the download URL
* @param localPath the path to save the download
* @param rename the file name to rename the download to when complete
*/
public Download(String remoteURL, String localPath, String rename) {
try {
this.url = new URL(remoteURL);
} catch (MalformedURLException e) {
this.status = Status.ERROR;
ErrorHandler.error(String.format("Bad download URL: '%s'", remoteURL), e, true);
return;
}
this.localPath = localPath;
this.rename = rename;
}
/**
* Starts the download from the "waiting" status.
*/
public void start() {
if (status != Status.WAITING)
return;
new Thread() {
@Override
public void run() {
// open connection, get content length
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
contentLength = conn.getContentLength();
} catch (IOException e) {
status = Status.ERROR;
ErrorHandler.error("Failed to open connection.", e, false);
return;
}
// download file
try (
InputStream in = conn.getInputStream();
ReadableByteChannel readableByteChannel = Channels.newChannel(in);
FileOutputStream fileOutputStream = new FileOutputStream(localPath);
) {
rbc = new ReadableByteChannelWrapper(readableByteChannel);
fos = fileOutputStream;
status = Status.DOWNLOADING;
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
if (status == Status.DOWNLOADING) { // not interrupted
status = Status.COMPLETE;
rbc.close();
fos.close();
if (rename != null) {
Path source = new File(localPath).toPath();
Files.move(source, source.resolveSibling(rename), StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (Exception e) {
status = Status.ERROR;
ErrorHandler.error("Failed to start download.", e, false);
}
}
}.start();
}
/**
* Returns the download status.
*/
public Status getStatus() { return status; }
/**
* Returns true if transfers are currently taking place.
*/
public boolean isTransferring() {
return (rbc != null && rbc.isOpen() && fos != null && fos.getChannel().isOpen());
}
/**
* Returns true if the download is active.
*/
public boolean isActive() {
return (status == Status.WAITING || status == Status.DOWNLOADING);
}
/**
* Returns the size of the download content in bytes, or -1 if not calculated
* (or if an error has occurred).
*/
public int contentLength() { return contentLength; }
/**
* Returns the download completion percentage, or -1f if an error has occurred.
*/
public float getProgress() {
switch (status) {
case WAITING:
return 0f;
case COMPLETE:
return 100f;
case DOWNLOADING:
if (rbc != null && fos != null && contentLength > 0)
return (float) rbc.getReadSoFar() / (float) contentLength * 100f;
else
return 0f;
case CANCELLED:
case ERROR:
default:
return -1f;
}
}
/**
* Returns the number of bytes read so far.
*/
public long readSoFar() {
switch (status) {
case COMPLETE:
return contentLength;
case DOWNLOADING:
if (rbc != null)
return rbc.getReadSoFar();
// else fall through
case WAITING:
case CANCELLED:
case ERROR:
default:
return 0;
}
}
/**
* Cancels the download, if running.
*/
public void cancel() {
try {
this.status = Status.CANCELLED;
boolean transferring = isTransferring();
if (rbc != null && rbc.isOpen())
rbc.close();
if (fos != null && fos.getChannel().isOpen())
fos.close();
if (transferring) {
File f = new File(localPath);
if (f.isFile())
f.delete();
}
} catch (IOException e) {
this.status = Status.ERROR;
ErrorHandler.error("Failed to cancel download.", e, true);
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import org.newdawn.slick.util.Log;
/**
* Maintains the current downloads list.
*/
public class DownloadList {
/** The single instance of this class. */
private static DownloadList list = new DownloadList();
/** Current list of downloads. */
private List<DownloadNode> nodes;
/** The map of beatmap set IDs to DownloadNodes for the current downloads. */
private Map<Integer, DownloadNode> map;
/**
* Returns the single instance of this class.
*/
public static DownloadList get() { return list; }
/**
* Constructor.
*/
private DownloadList() {
nodes = new ArrayList<DownloadNode>();
map = new HashMap<Integer, DownloadNode>();
}
/**
* Returns the DownloadNode at an index.
*/
public DownloadNode getNode(int index) { return nodes.get(index); }
/**
* Gets the Download for a beatmap set ID, or null if not in the list.
*/
public Download getDownload(int beatmapSetID) {
DownloadNode node = map.get(beatmapSetID);
return (node == null) ? null : node.getDownload();
}
/**
* Returns the size of the doownloads list.
*/
public int size() { return nodes.size(); }
/**
* Returns {@code true} if this list contains no elements.
*/
public boolean isEmpty() { return nodes.isEmpty(); }
/**
* Returns {@code true} if this list contains the beatmap set ID.
*/
public boolean contains(int beatmapSetID) { return map.containsKey(beatmapSetID); }
/**
* Adds a DownloadNode to the list.
*/
public void addNode(DownloadNode node) {
nodes.add(node);
map.put(node.getID(), node);
}
/**
* Removes a DownloadNode from the list.
*/
public void remove(DownloadNode node) { remove(nodes.indexOf(node)); }
/**
* Removes a DownloadNode from the list at the given index.
*/
public void remove(int index) {
DownloadNode node = nodes.remove(index);
map.remove(node.getID());
}
/**
* Returns {@code true} if the list contains any downloads that are active.
*/
public boolean hasActiveDownloads() {
for (DownloadNode node: nodes) {
Download dl = node.getDownload();
if (dl != null && dl.isActive())
return true;
}
return false;
}
/**
* Cancels all downloads.
*/
public void cancelAllDownloads() {
for (DownloadNode node : nodes) {
Download dl = node.getDownload();
if (dl != null && dl.isActive())
dl.cancel();
}
}
/**
* Shows a confirmation dialog (used before exiting the game).
* @return true if user selects "yes", false otherwise
*/
public static boolean showExitConfirmation() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
Log.error("Could not set system look and feel for Container.", e);
}
int n = JOptionPane.showConfirmDialog(null,
"Beatmap downloads are in progress.\nAre you sure you want to quit opsu!?",
"Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
return (n != JOptionPane.YES_OPTION);
}
}

View File

@@ -0,0 +1,396 @@
/*
* 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;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.Download.Status;
import itdelatrisu.opsu.states.DownloadsMenu;
import java.io.File;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
/**
* Node containing song data and a Download object.
*/
public class DownloadNode {
/** The associated Download object. */
private Download download;
/** Beatmap set ID. */
private int beatmapSetID;
/** Last updated date string. */
private String date;
/** Song title. */
private String title, titleUnicode;
/** Song artist. */
private String artist, artistUnicode;
/** Beatmap creator. */
private String creator;
/** Button drawing values. */
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
/** Information drawing values. */
private static float infoBaseX, infoBaseY, infoWidth, infoHeight;
/** Container dimensions. */
private static int containerWidth, containerHeight;
/** 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
* @param height the container height
*/
public static void init(int width, int height) {
containerWidth = width;
containerHeight = height;
// download result buttons
buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f;
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2f;
buttonOffset = buttonHeight * 1.1f;
// download info
infoBaseX = width * 0.75f;
infoBaseY = height * 0.07f + Utils.FONT_LARGE.getLineHeight() * 2f;
infoWidth = width * 0.25f;
infoHeight = Utils.FONT_DEFAULT.getLineHeight() * 2.4f;
}
/**
* Returns true if the coordinates are within the bounds of the
* download result button at the given index.
* @param cx the x coordinate
* @param cy the y coordinate
* @param index the index (to offset the button from the topmost button)
*/
public static boolean resultContains(float cx, float cy, int index) {
float y = buttonBaseY + (index * buttonOffset);
return ((cx > buttonBaseX && cx < buttonBaseX + buttonWidth) &&
(cy > y && cy < y + buttonHeight));
}
/**
* Returns true if the coordinates are within the bounds of the
* download result button area.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean resultAreaContains(float cx, float cy) {
return ((cx > buttonBaseX && cx < buttonBaseX + buttonWidth) &&
(cy > buttonBaseY && cy < buttonBaseY + buttonOffset * DownloadsMenu.MAX_RESULT_BUTTONS));
}
/**
* Returns true if the coordinates are within the bounds of the
* download information button at the given index.
* @param cx the x coordinate
* @param cy the y coordinate
* @param index the index (to offset the button from the topmost button)
*/
public static boolean downloadContains(float cx, float cy, int index) {
float y = infoBaseY + (index * infoHeight);
return ((cx > infoBaseX && cx <= containerWidth) &&
(cy > y && cy < y + infoHeight));
}
/**
* Returns true if the coordinates are within the bounds of the
* download action icon at the given index.
* @param cx the x coordinate
* @param cy the y coordinate
* @param index the index (to offset the button from the topmost button)
*/
public static boolean downloadIconContains(float cx, float cy, int index) {
int iconWidth = GameImage.DELETE.getImage().getWidth();
float edgeX = infoBaseX + infoWidth * 0.985f;
float y = infoBaseY + (index * infoHeight);
float marginY = infoHeight * 0.04f;
return ((cx > edgeX - iconWidth && cx < edgeX) &&
(cy > y + marginY && cy < y + marginY + iconWidth));
}
/**
* Returns true if the coordinates are within the bounds of the
* download information button area.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean downloadAreaContains(float cx, float cy) {
return ((cx > infoBaseX && cx <= containerWidth) &&
(cy > infoBaseY && cy < infoBaseY + infoHeight * DownloadsMenu.MAX_DOWNLOADS_SHOWN));
}
/**
* Returns true if the coordinates are within the bounds of the
* previous page icon.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean prevPageContains(float cx, float cy) {
Image img = GameImage.MUSIC_PREVIOUS.getImage();
return ((cx > buttonBaseX && cx < buttonBaseX + img.getWidth()) &&
(cy > buttonBaseY - img.getHeight() && cy < buttonBaseY));
}
/**
* Returns true if the coordinates are within the bounds of the
* next page icon.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public static boolean nextPageContains(float cx, float cy) {
Image img = GameImage.MUSIC_NEXT.getImage();
return ((cx > buttonBaseX + buttonWidth - img.getWidth() && cx < buttonBaseX + buttonWidth) &&
(cy > buttonBaseY - img.getHeight() && cy < buttonBaseY));
}
/**
* Draws the scroll bar for the download result buttons.
* @param g the graphics context
* @param index the start button index
* @param total the total number of buttons
*/
public static void drawResultScrollbar(Graphics g, int index, int total) {
float scrollbarWidth = containerWidth * 0.00347f;
float heightRatio = 0.0016f * (total * total) - 0.0705f * total + 0.9965f;
float scrollbarHeight = containerHeight * heightRatio;
float heightDiff = buttonHeight + buttonOffset * (DownloadsMenu.MAX_RESULT_BUTTONS - 1) - scrollbarHeight;
float offsetY = heightDiff * ((float) index / (total - DownloadsMenu.MAX_RESULT_BUTTONS));
g.setColor(BG_NORMAL);
g.fillRect(buttonBaseX + buttonWidth * 1.005f, buttonBaseY, scrollbarWidth, buttonOffset * DownloadsMenu.MAX_RESULT_BUTTONS);
g.setColor(Color.white);
g.fillRect(buttonBaseX + buttonWidth * 1.005f, buttonBaseY + offsetY, scrollbarWidth, scrollbarHeight);
}
/**
* Draws the scroll bar for the download information area.
* @param g the graphics context
* @param index the start index
* @param total the total number of downloads
*/
public static void drawDownloadScrollbar(Graphics g, int index, int total) {
float scrollbarWidth = containerWidth * 0.00347f;
float heightRatio = 0.0016f * (total * total) - 0.0705f * total + 0.9965f;
float scrollbarHeight = containerHeight * heightRatio;
float heightDiff = infoHeight + infoHeight * (DownloadsMenu.MAX_DOWNLOADS_SHOWN - 1) - scrollbarHeight;
float offsetY = heightDiff * ((float) index / (total - DownloadsMenu.MAX_DOWNLOADS_SHOWN));
g.setColor(BG_NORMAL);
g.fillRect(infoBaseX + infoWidth - scrollbarWidth, infoBaseY, scrollbarWidth, infoHeight * DownloadsMenu.MAX_DOWNLOADS_SHOWN);
g.setColor(Color.white);
g.fillRect(infoBaseX + infoWidth - scrollbarWidth, infoBaseY + offsetY, scrollbarWidth, scrollbarHeight);
}
/**
* Draws the page number text and previous/next page icons.
* @param page the current page number
* @param prev whether to draw the previous page icon
* @param next whether to draw the next page icon
*/
public static void drawPageIcons(int page, boolean prev, boolean next) {
String pageText = String.format("Page %d", page);
Utils.FONT_BOLD.drawString(
buttonBaseX + (buttonWidth - Utils.FONT_BOLD.getWidth("Page 1")) / 2f,
buttonBaseY - Utils.FONT_BOLD.getLineHeight() * 1.3f, pageText, Color.white);
if (prev) {
Image prevImg = GameImage.MUSIC_PREVIOUS.getImage();
prevImg.draw(buttonBaseX, buttonBaseY - prevImg.getHeight());
}
if (next) {
Image nextImg = GameImage.MUSIC_NEXT.getImage();
nextImg.draw(buttonBaseX + buttonWidth - nextImg.getWidth(), buttonBaseY - nextImg.getHeight());
}
}
/**
* Constructor.
*/
public DownloadNode(int beatmapSetID, String date, String title,
String titleUnicode, String artist, String artistUnicode, String creator) {
this.beatmapSetID = beatmapSetID;
this.date = date;
this.title = title;
this.titleUnicode = titleUnicode;
this.artist = artist;
this.artistUnicode = artistUnicode;
this.creator = creator;
}
/**
* Creates a download object for this node.
* @param server the server to download from
* @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);
}
}
/**
* Returns the associated download object, or null if none.
* @see #createDownload(DownloadServer)
*/
public Download getDownload() { return download; }
/**
* Clears the associated download object, if any.
* @see #createDownload(DownloadServer)
*/
public void clearDownload() { download = null; }
/**
* Returns the beatmap set ID.
*/
public int getID() { return beatmapSetID; }
/**
* Returns the last updated date.
*/
public String getDate() { return date; }
/**
* Returns the song title.
* If configured, the Unicode string will be returned instead.
*/
public String getTitle() {
return (Options.useUnicodeMetadata() && titleUnicode != null && !titleUnicode.isEmpty()) ? titleUnicode : title;
}
/**
* Returns the song artist.
* If configured, the Unicode string will be returned instead.
*/
public String getArtist() {
return (Options.useUnicodeMetadata() && artistUnicode != null && !artistUnicode.isEmpty()) ? artistUnicode : artist;
}
/**
* Returns the song creator.
*/
public String getCreator() { return creator; }
/**
* Draws the download result as a rectangular button.
* @param g the graphics context
* @param index the index (to offset the button from the topmost button)
* @param hover true if the mouse is hovering over this button
* @param focus true if the button is focused
*/
public void drawResult(Graphics g, int index, boolean hover, boolean focus) {
float textX = buttonBaseX + buttonWidth * 0.02f;
float edgeX = buttonBaseX + buttonWidth * 0.985f;
float y = buttonBaseY + index * buttonOffset;
float marginY = buttonHeight * 0.04f;
Download dl = DownloadList.get().getDownload(beatmapSetID);
// rectangle outline
g.setColor((focus) ? BG_FOCUS : (hover) ? BG_HOVER : BG_NORMAL);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// download progress
if (dl != null) {
float progress = dl.getProgress();
if (progress > 0f) {
g.setColor(Utils.COLOR_GREEN);
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
}
}
// text
Utils.FONT_BOLD.drawString(
textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white);
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
creator, Color.white);
}
/**
* Draws the download information.
* @param g the graphics context
* @param index the index (to offset from the topmost position)
* @param id the list index
* @param hover true if the mouse is hovering over this button
*/
public void drawDownload(Graphics g, int index, int id, boolean hover) {
if (download == null) {
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
return;
}
float textX = infoBaseX + infoWidth * 0.02f;
float edgeX = infoBaseX + infoWidth * 0.985f;
float y = infoBaseY + index * infoHeight;
float marginY = infoHeight * 0.04f;
// rectangle outline
g.setColor((id % 2 == 0) ? BG_FOCUS : BG_NORMAL);
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
// text
String info;
Status status = download.getStatus();
float progress = download.getProgress();
if (progress < 0f)
info = status.getName();
else if (status == Download.Status.WAITING)
info = String.format("%s...", status.getName());
else
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);
// 'x' button
if (hover) {
Image img = GameImage.DELETE.getImage();
img.draw(edgeX - img.getWidth(), y + marginY);
}
}
@Override
public String toString() {
return String.format("[%d] %s - %s (by %s)", beatmapSetID, getArtist(), getTitle(), creator);
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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;
import java.io.IOException;
/**
* Interface for beatmap download servers.
*/
public interface DownloadServer {
/**
* Returns a web address to download the given beatmap.
* @param beatmapSetID the beatmap set ID
* @return the URL string
*/
public String getURL(int beatmapSetID);
/**
* Returns a list of results for a given search query, or null if the
* list could not be created.
* @param query the search query
* @param page the result page (starting at 1)
* @param rankedOnly whether to only show ranked maps
* @return the result array
* @throws IOException if any connection problem occurs
*/
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException;
/**
* Returns the total number of results for the last search query.
* This will differ from the the size of the array returned by
* {@link #resultList(String, int, boolean)} if multiple pages exist.
* @return the result count, or -1 if no query
*/
public int totalResults();
}

View File

@@ -0,0 +1,62 @@
/*
* 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;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
/**
* Wrapper for a ReadableByteChannel that stores the number of bytes read.
* @author par (http://stackoverflow.com/a/11068356)
*/
public class ReadableByteChannelWrapper implements ReadableByteChannel {
/** The wrapped ReadableByteChannel. */
private ReadableByteChannel rbc;
/** The number of bytes read. */
private long bytesRead;
/**
* Constructor.
* @param rbc the ReadableByteChannel to wrap
*/
public ReadableByteChannelWrapper(ReadableByteChannel rbc) {
this.rbc = rbc;
}
@Override
public void close() throws IOException { rbc.close(); }
@Override
public boolean isOpen() { return rbc.isOpen(); }
@Override
public int read(ByteBuffer bb) throws IOException {
int bytes;
if ((bytes = rbc.read(bb)) > 0)
bytesRead += bytes;
return bytes;
}
/**
* Returns the number of bytes read so far.
*/
public long getReadSoFar() { return bytesRead; }
}