diff --git a/.gitignore b/.gitignore index 4ba1441d..6348415d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /Skins/ /SongPacks/ /Songs/ +/Temp/ /.opsu.log /.opsu.cfg /.opsu.db* diff --git a/README.md b/README.md index cc1455a2..d21f1585 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The following files and folders will be created by opsu! as needed: files within this directory to the replay directory and saves the scores in the scores database. Replays can be imported from osu! as well as opsu!. * `Natives/`: The native libraries directory. +* `Temp/`: The temporary files directory. Deleted when opsu! exits. ## Building opsu! is distributed as both a [Maven](https://maven.apache.org/) and diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index 5fbb774b..67a4daec 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -27,11 +27,15 @@ import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.render.CurveRenderState; import itdelatrisu.opsu.ui.UI; +import java.io.IOException; + +import org.codehaus.plexus.util.FileUtils; import org.lwjgl.opengl.Display; import org.newdawn.slick.AppGameContainer; import org.newdawn.slick.Game; import org.newdawn.slick.SlickException; import org.newdawn.slick.opengl.InternalTextureLoader; +import org.newdawn.slick.util.Log; /** * AppGameContainer extension that sends critical errors to ErrorHandler. @@ -142,6 +146,15 @@ public class Container extends AppGameContainer { if (!Options.isWatchServiceEnabled()) BeatmapWatchService.destroy(); BeatmapWatchService.removeListeners(); + + // delete temporary directory + if (Options.TEMP_DIR.isDirectory()) { + try { + FileUtils.deleteDirectory(Options.TEMP_DIR); + } catch (IOException e) { + Log.warn(String.format("Failed to delete temp dir: %s", Options.TEMP_DIR.getAbsolutePath()), e); + } + } } @Override diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 9bf79fac..1ec253c4 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -94,6 +94,9 @@ public class Options { /** Directory where natives are unpacked. */ public static final File NATIVE_DIR = new File(CACHE_DIR, "Natives/"); + /** Directory where temporary files are stored (deleted on exit). */ + public static final File TEMP_DIR = new File(CACHE_DIR, "Temp/"); + /** Font file name. */ public static final String FONT_NAME = "DroidSansFallback.ttf"; diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java index 7f42f144..8a0cbe0e 100644 --- a/src/itdelatrisu/opsu/audio/SoundController.java +++ b/src/itdelatrisu/opsu/audio/SoundController.java @@ -22,6 +22,9 @@ import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.audio.HitSound.SampleSet; import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.downloads.Download; +import itdelatrisu.opsu.downloads.Download.DownloadListener; +import itdelatrisu.opsu.ui.UI; import java.io.File; import java.io.IOException; @@ -345,24 +348,58 @@ public class SoundController { } /** - * Plays a track from a URL. + * Plays a track from a remote URL. * If a track is currently playing, it will be stopped. - * @param url the resource URL + * @param url the remote URL + * @param name the track file name * @param isMP3 true if MP3, false if WAV * @param listener the line listener - * @return the MultiClip being played + * @return true if playing, false otherwise * @throws SlickException if any error occurred */ - public static synchronized MultiClip playTrack(URL url, boolean isMP3, LineListener listener) throws SlickException { + public static synchronized boolean playTrack(String url, String name, boolean isMP3, LineListener listener) + throws SlickException { + // stop previous track stopTrack(); - try { - AudioInputStream audioIn = AudioSystem.getAudioInputStream(url); - currentTrack = loadClip(url.getFile(), audioIn, isMP3); - playClip(currentTrack, Options.getMusicVolume() * Options.getMasterVolume(), listener); - return currentTrack; - } catch (Exception e) { - throw new SlickException(String.format("Failed to load clip '%s'.", url.getFile(), e)); + + // download new track + File dir = Options.TEMP_DIR; + if (!dir.isDirectory()) + dir.mkdir(); + String filename = String.format("%s.%s", name, isMP3 ? "mp3" : "wav"); + final File downloadFile = new File(dir, filename); + boolean complete; + if (downloadFile.isFile()) { + complete = true; // file already downloaded + } else { + Download download = new Download(url, downloadFile.getAbsolutePath()); + download.setListener(new DownloadListener() { + @Override + public void completed() {} + + @Override + public void error() { + UI.sendBarNotification("Failed to download track preview."); + } + }); + try { + download.start().join(); + } catch (InterruptedException e) {} + complete = (download.getStatus() == Download.Status.COMPLETE); } + + // play the track + if (complete) { + try { + AudioInputStream audioIn = AudioSystem.getAudioInputStream(downloadFile); + currentTrack = loadClip(filename, audioIn, isMP3); + playClip(currentTrack, Options.getMusicVolume() * Options.getMasterVolume(), listener); + return true; + } catch (Exception e) { + throw new SlickException(String.format("Failed to load clip '%s'.", url)); + } + } + return false; } /** diff --git a/src/itdelatrisu/opsu/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index 09ebe292..58818490 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -167,12 +167,13 @@ public class Download { /** * Starts the download from the "waiting" status. + * @return the started download thread, or {@code null} if none started */ - public void start() { + public Thread start() { if (status != Status.WAITING) - return; + return null; - new Thread() { + Thread t = new Thread() { @Override public void run() { // open connection @@ -274,7 +275,9 @@ public class Download { listener.error(); } } - }.start(); + }; + t.start(); + return t; } /** diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index a9c5b7cf..62f0eff6 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -47,8 +47,6 @@ import itdelatrisu.opsu.ui.UI; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; @@ -663,15 +661,18 @@ public class DownloadsMenu extends BasicGameState { SoundController.stopTrack(); } else { // play preview - try { - final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID())); - MusicController.pause(); - new Thread() { - @Override - public void run() { - try { - previewID = -1; - SoundController.playTrack(url, true, new LineListener() { + final String url = serverMenu.getSelectedItem().getPreviewURL(node.getID()); + MusicController.pause(); + new Thread() { + @Override + public void run() { + try { + previewID = -1; + boolean playing = SoundController.playTrack( + url, + Integer.toString(node.getID()), + true, + new LineListener() { @Override public void update(LineEvent event) { if (event.getType() == LineEvent.Type.STOP) { @@ -681,18 +682,16 @@ public class DownloadsMenu extends BasicGameState { } } } - }); + } + ); + if (playing) previewID = node.getID(); - } catch (SlickException e) { - UI.sendBarNotification("Failed to load track preview. See log for details."); - Log.error(e); - } + } catch (SlickException e) { + UI.sendBarNotification("Failed to load track preview. See log for details."); + Log.error(e); } - }.start(); - } catch (MalformedURLException e) { - UI.sendBarNotification("Could not load track preview (bad URL)."); - Log.error(e); - } + } + }.start(); } return; }