diff --git a/README.md b/README.md new file mode 100644 index 0000000..73f49c4 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# CMPDL + +**Curse Modpack Downloader** + +Downloads modpacks from curse, given a URL. Experimental. Forked from Vazkii's project with a new UI. + +Licensed under the [WTFPL](http://www.wtfpl.net/). Have fun. + +## Usage Instructions + + * Download CMPDL on the [Releases](https://github.com/Franckyi/CMPDL/releases) page. Run it. + * Find the pack you want. We'll use [Proton](https://minecraft.curseforge.com/projects/proton) as an example. + * Copy the URL of the project home on CurseForge. It should look like the one on the last point. Paste this URL on the "Modpack URL" field on CMPDL. + * To specify a version of the modpack, enter that version in the "File ID" field + * Choose the modpack destination. If you use MultiMC, choose a new folder under the "instances" folder. + * Press "Start Download" and wait. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ed9c534 --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.github.franckyi + cmpdl + 2.0.0-b1 + + 1.8 + 1.8 + + + + com.google.code.gson + gson + 2.8.1 + + + + + + maven-assembly-plugin + + + + com.github.franckyi.cmpdl.CMPDL + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/github/franckyi/cmpdl/CMPDL.java b/src/main/java/com/github/franckyi/cmpdl/CMPDL.java new file mode 100644 index 0000000..c7b018c --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/CMPDL.java @@ -0,0 +1,89 @@ +package com.github.franckyi.cmpdl; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.io.File; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +public class CMPDL extends Application { + + private static final String NAME = "Curse Modpack Downloader"; + private static final String VERSION = "2.0.0-b1"; + private static final String AUTHOR = "Franckyi (original version by Vazkii)"; + + public static String title() { + return String.format("%s v%s by %s", NAME, VERSION, AUTHOR); + } + + public static InterfaceController controller; + public static Parent parent; + public static Stage stage; + public static Gson gson; + + public static String path; + public static String zipFileName; + + public static final Set exceptions = new HashSet<>(); + + public void start(Stage primaryStage) throws Exception { + stage = primaryStage; + gson = new GsonBuilder().create(); + URL interface0 = getClass().getClassLoader().getResource("interface.fxml"); + if (interface0 != null) { + FXMLLoader loader = new FXMLLoader(interface0); + parent = loader.load(); + controller = loader.getController(); + stage.setScene(new Scene(parent)); + stage.setResizable(false); + stage.setTitle(title()); + stage.show(); + } else { + throw new RuntimeException("Impossible to load interface.fxml file : the application can't start"); + } + } + + @Override + public void stop() throws Exception { + controller.stop(null); + } + + public static void main(String[] args) { + launch(args); + } + + public static String getTempDirectory() { + return path + File.separator + ".cmpdl_temp"; + } + + public static String getZipFile() { + return getTempDirectory() + File.separator + zipFileName; + } + + private static String getMinecraftDirectory() { + return path + File.separator + "minecraft"; + } + + public static String getModsDirectory() { + return getMinecraftDirectory() + File.separator + "mods"; + } + + public static String getInstanceFile() { + return path + File.separator + "instance.cfg"; + } + + public static String getManifestFile() { + return getTempDirectory() + File.separator + "manifest.json"; + } + + public static String toOverridePath(File file, String override) { + return file.getPath().replace(".cmpdl_temp" + File.separator + override, "minecraft"); + } +} diff --git a/src/main/java/com/github/franckyi/cmpdl/InterfaceController.java b/src/main/java/com/github/franckyi/cmpdl/InterfaceController.java new file mode 100644 index 0000000..0253cd7 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/InterfaceController.java @@ -0,0 +1,264 @@ +package com.github.franckyi.cmpdl; + + +import com.github.franckyi.cmpdl.task.CleanTask; +import com.github.franckyi.cmpdl.task.DownloadModpackTask; +import com.github.franckyi.cmpdl.task.VerifyProjectTask; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.stage.DirectoryChooser; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.ResourceBundle; + +@SuppressWarnings("Duplicates") +public class InterfaceController implements Initializable { + + private boolean dark; + private Optional verified = Optional.empty(); + private Task primaryTask, secondaryTask; + + @Override + public void initialize(URL location, ResourceBundle resources) { + log(CMPDL.title()); + log("Java version " + System.getProperty("java.version")); + destinationPath.setText(System.getProperty("user.home") + File.separator + "modpack"); + } + + @FXML + private TextField modpackURL; + + @FXML + private TextField fileID; + + @FXML + public TextField destinationPath; + + @FXML + private TextArea logTextArea; + + @FXML + private ProgressBar primaryBar; + + @FXML + private ProgressBar secondaryBar; + + @FXML + private ProgressIndicator primaryIndicator; + + @FXML + private ProgressIndicator secondaryIndicator; + + @FXML + private Label primaryLabel; + + @FXML + private Label secondaryLabel; + + @FXML + private Button verifyButton; + + @FXML + private Button destinationButton; + + @FXML + private Button downloadButton; + + @FXML + private Button stopButton; + + @FXML + void chooseDestination(ActionEvent event) { + DirectoryChooser directoryChooser = new DirectoryChooser(); + File dir = new File(destinationPath.getText()); + directoryChooser.setInitialDirectory(dir.exists() ? dir : new File(System.getProperty("user.home"))); + directoryChooser.setTitle("Choose the output game / instance directory :"); + File file = directoryChooser.showDialog(CMPDL.stage); + if (file != null) { + destinationPath.setText(file.getAbsolutePath()); + log("Destination : " + file.getAbsolutePath()); + } + } + + @FXML + void logClear(ActionEvent event) { + logTextArea.setText(""); + } + + @FXML + void logCopy(ActionEvent event) { + ClipboardContent content = new ClipboardContent(); + content.putString(logTextArea.getText()); + Clipboard.getSystemClipboard().setContent(content); + } + + @FXML + void startDownload(ActionEvent event) { + setDisableButtons(true); + if (!verified.isPresent()) { + downloadButton.setText("Verifying..."); + VerifyProjectTask task = new VerifyProjectTask(fileID.getText(), modpackURL.getText()); + task.setOnSucceeded(e -> { + log(task.getValue().isPresent() ? "The modpack is valid !" : "The modpack is invalid !"); + if (task.getValue().isPresent()) { + verified = task.getValue(); + startDownload0(); + } else { + setDisableButtons(false); + downloadButton.setText("Start Download"); + } + }); + new Thread(task).start(); + } else startDownload0(); + } + + private void startDownload0() { + log("Download starting !"); + downloadButton.setText("Downloading..."); + stopButton.setDisable(false); + verified.ifPresent(s -> { + try { + CMPDL.path = destinationPath.getText(); + CMPDL.zipFileName = new File(new URL(s).getFile()).getName().replaceAll("%20", " "); + setPrimaryProgress(new DownloadModpackTask(s), "Step 1/3 : Downloading modpack"); + new Thread(primaryTask).start(); + } catch (IOException e) { + trace(e); + } + }); + } + + @FXML + void verify(ActionEvent event) { + verifyButton.setText("Verifying..."); + setDisableButtons(true); + VerifyProjectTask task = new VerifyProjectTask(fileID.getText(), modpackURL.getText()); + task.setOnSucceeded(e -> { + log(task.getValue().isPresent() ? "The modpack is valid !" : "The modpack is invalid !"); + if (task.getValue().isPresent()) { + verified = task.getValue(); + verifyButton.setText("Verified !"); + setDisableButtons(false); + verifyButton.setDisable(true); + } else { + verifyButton.setText("Verify"); + setDisableButtons(false); + } + }); + new Thread(task).start(); + } + + @FXML + void input(KeyEvent keyEvent) { + verified = Optional.empty(); + verifyButton.setText("Verify"); + verifyButton.setDisable(false); + } + + @FXML + void changeTheme(MouseEvent mouseEvent) { + CMPDL.parent.setStyle(dark ? "" : "-fx-base: rgba(60, 63, 65, 255);"); + dark = !dark; + } + + public void log(String text) { + logTextArea.appendText(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yy HH:mm:ss > ")) + text + "\n"); + } + + public void trace(Throwable t) { + logTextArea.appendText(t.toString() + "\n"); + for (StackTraceElement traceElement : t.getStackTrace()) + logTextArea.appendText("\tat " + traceElement + "\n"); + if (t.getCause() != null) { + trace(t.getCause()); + } + } + + private void setDisableButtons(boolean value) { + downloadButton.setDisable(value); + verifyButton.setDisable(value); + destinationButton.setDisable(value); + modpackURL.setEditable(value); + fileID.setEditable(value); + destinationPath.setEditable(value); + } + + public void setPrimaryProgress(Task task, String text) { + primaryTask = task; + primaryBar.progressProperty().bind(task.progressProperty()); + primaryIndicator.progressProperty().bind(task.progressProperty()); + primaryLabel.setText(text); + } + + private void resetPrimaryProgress() { + primaryTask = null; + primaryBar.progressProperty().unbind(); + primaryBar.setProgress(0); + primaryIndicator.progressProperty().unbind(); + primaryIndicator.setProgress(0); + primaryLabel.setText(""); + } + + public void setSecondaryProgress(Task task, String text) { + secondaryTask = task; + secondaryBar.progressProperty().bind(task.progressProperty()); + secondaryIndicator.progressProperty().bind(task.progressProperty()); + secondaryLabel.setText(text); + } + + private void resetSecondaryProgress() { + secondaryTask = null; + secondaryBar.progressProperty().unbind(); + secondaryBar.setProgress(0); + secondaryIndicator.progressProperty().unbind(); + secondaryIndicator.setProgress(0); + secondaryLabel.setText(""); + } + + @FXML + void stop(ActionEvent actionEvent) { + if (primaryTask != null) { + if (primaryTask.isDone() || primaryTask.cancel()) { + log("Primary task cancelled"); + resetPrimaryProgress(); + } else + log("Couldn't cancel primary task"); + + } + if (secondaryTask != null) { + if (secondaryTask.isDone() || secondaryTask.cancel()) { + log("Secondary task cancelled"); + resetSecondaryProgress(); + } else + log("Couldn't cancel secondary task"); + } + if ((primaryTask == null || primaryTask.isCancelled() || primaryTask.isDone()) && (secondaryTask == null || secondaryTask.isCancelled() || secondaryTask.isDone())) { + setDisableButtons(false); + log("Execution stopped !"); + new Thread(CleanTask::new).start(); + downloadButton.setText("Start Download"); + } + } + + public void reset() { + CMPDL.exceptions.clear(); + resetPrimaryProgress(); + resetSecondaryProgress(); + setDisableButtons(false); + stopButton.setDisable(true); + downloadButton.setText("Start Download"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/github/franckyi/cmpdl/ManifestJson.java b/src/main/java/com/github/franckyi/cmpdl/ManifestJson.java new file mode 100644 index 0000000..d1db733 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/ManifestJson.java @@ -0,0 +1,51 @@ +package com.github.franckyi.cmpdl; + +import java.util.List; + +@SuppressWarnings("unused") +public class ManifestJson { + + public MinecraftJson minecraft; + + public String getForgeVersion() { + for (MinecraftJson.ModloaderJson loader : minecraft.modLoaders) { + if (loader.id.startsWith("forge")) + return loader.id.substring("forge-".length()); + } + return "N/A"; + } + + @SuppressWarnings("unused") + public class MinecraftJson { + + public String version; + public List modLoaders; + + @SuppressWarnings("unused") + public class ModloaderJson { + + public String id; + public boolean primary; + + } + } + + public String manifestType; + public int manifestVersion; + public String name; + public String version; + public String author; + public List files; + + @SuppressWarnings("unused") + public class FileJson { + + public int projectID; + public int fileID; + public boolean required; + + } + + public String overrides; + +} diff --git a/src/main/java/com/github/franckyi/cmpdl/task/CleanTask.java b/src/main/java/com/github/franckyi/cmpdl/task/CleanTask.java new file mode 100644 index 0000000..80e6571 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/CleanTask.java @@ -0,0 +1,43 @@ +package com.github.franckyi.cmpdl.task; + +import com.github.franckyi.cmpdl.CMPDL; +import com.github.franckyi.cmpdl.ManifestJson; + +import java.io.File; +import java.util.Arrays; + +public class CleanTask extends CustomTask { + + private ManifestJson manifest; + + public CleanTask(ManifestJson manifest) { + this.manifest = manifest; + } + + public CleanTask() { + } + + @Override + protected Void call0() throws Exception { + log("> Cleaning up..."); + del(new File(CMPDL.getTempDirectory())); + return null; + } + + @Override + protected void succeeded() { + if (manifest != null) { + log("### RECOMMENDED FORGE VERSION : " + manifest.getForgeVersion() + ". ###"); + log("### A newer version should also work."); + log("### You must install it manually if you're using MultiMC ! ###"); + } + getController().reset(); + log(CMPDL.exceptions.isEmpty() ? "Done !" : "Done with " + CMPDL.exceptions.size() + " error(s). See log for more info."); + log("----------------"); + } + + private void del(File file) { + if (file.isDirectory()) Arrays.asList(file.listFiles()).forEach(this::del); + file.delete(); + } +} diff --git a/src/main/java/com/github/franckyi/cmpdl/task/CopyOverridesTask.java b/src/main/java/com/github/franckyi/cmpdl/task/CopyOverridesTask.java new file mode 100644 index 0000000..8f5cee8 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/CopyOverridesTask.java @@ -0,0 +1,111 @@ +package com.github.franckyi.cmpdl.task; + +import com.github.franckyi.cmpdl.CMPDL; +import com.github.franckyi.cmpdl.ManifestJson; +import javafx.application.Platform; + +import java.io.*; +import java.util.Arrays; + +public class CopyOverridesTask extends CustomTask { + + private final ManifestJson manifest; + private final File dir; + private long curSize = 0L; + private final long maxSize; + + public CopyOverridesTask(ManifestJson manifest, File folder) { + this.manifest = manifest; + this.dir = new File(folder + File.separator + manifest.overrides); + this.maxSize = dir.length(); + } + + @Override + protected Void call0() throws Exception { + log("> Copying overrides"); + if (dir.isDirectory()) { + for (File file : dir.listFiles()) { + if (!processFile(file)) return null; + } + } + File cfg = new File(CMPDL.getInstanceFile()); + if (!cfg.exists()) + cfg.createNewFile(); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(cfg))) { + writer.write("InstanceType=OneSix\n" + + "IntendedVersion=" + manifest.minecraft.version + "\n" + + "LogPrePostOutput=true\n" + + "OverrideCommands=false\n" + + "OverrideConsole=false\n" + + "OverrideJavaArgs=false\n" + + "OverrideJavaLocation=false\n" + + "OverrideMemory=false\n" + + "OverrideWindow=false\n" + + "iconKey=default\n" + + "lastLaunchTime=0\n" + + "name=" + manifest.name + " " + manifest.version + "\n" + + "notes=Modpack by " + manifest.author + ". Generated by CMPDL. Using Forge " + manifest.getForgeVersion() + ".\n" + + "totalTimePlayed=0\n"); + } + return null; + } + + private boolean processFile(File file) { + if (file.isDirectory()) { + Arrays.asList(file.listFiles()).forEach(this::processFile); + } else { + File dest = new File(CMPDL.toOverridePath(file, manifest.overrides)); + new File(dest.getPath().replace(dest.getName(), "")).mkdirs(); + try { + dest.createNewFile(); + } catch (IOException e) { + trace(e); + e.printStackTrace(); + } + CopyOverrideTask task = new CopyOverrideTask(file, dest); + Platform.runLater(() -> getController().setSecondaryProgress(task, file.getName())); + new Thread(task).start(); + while (!task.isDone() && !task.isCancelled()) { + if (task.isCancelled()) return false; + } + } + return true; + } + + @Override + protected void succeeded() { + log("> Done copying overrides"); + new Thread(new CleanTask(manifest)).start(); + } + + private class CopyOverrideTask extends CustomTask { + + private final File source; + private final File dest; + + public CopyOverrideTask(File source, File dest) { + this.source = source; + this.dest = dest; + } + + @Override + protected Void call0() throws Exception { + try (FileInputStream in = new FileInputStream(source)) { + FileOutputStream out = new FileOutputStream(dest); + long curSize2 = 0L; + long maxSize2 = source.length(); + byte[] buffer = new byte[1024]; + int length; + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + updateProgress(curSize2 += length, maxSize2); + CopyOverridesTask.this.updateProgress(curSize += curSize2, maxSize); + } + out.close(); + } + log("> > Copied " + dest.getName()); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/franckyi/cmpdl/task/CustomTask.java b/src/main/java/com/github/franckyi/cmpdl/task/CustomTask.java new file mode 100644 index 0000000..63260a4 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/CustomTask.java @@ -0,0 +1,58 @@ +package com.github.franckyi.cmpdl.task; + +import com.github.franckyi.cmpdl.CMPDL; +import com.github.franckyi.cmpdl.InterfaceController; +import javafx.application.Platform; +import javafx.concurrent.Task; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +abstract class CustomTask extends Task { + + @Override + protected V call() throws Exception { + try { + return call0(); + } catch (Exception e) { + e.printStackTrace(); + CMPDL.exceptions.add(e); + CMPDL.controller.trace(e); + } + return null; + } + + protected abstract V call0() throws Exception; + + void log(String s) { + Platform.runLater(() -> getController().log(s)); + } + + void trace(Throwable t) { + Platform.runLater(() -> getController().trace(t)); + } + + InterfaceController getController() { + return CMPDL.controller; + } + + String crawl(String url) throws IOException, IllegalArgumentException { + String location = getLocation(url); + return location != null ? crawl(location) : url; + } + + String crawlAddHost(String url) throws IOException, IllegalArgumentException { + URL url0 = new URL(url); + String location = getLocation(url); + return location != null ? location.contains("://") ? crawlAddHost(location) : crawl(url0.getProtocol() + "://" + url0.getHost() + location) : url; + } + + private String getLocation(String url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setInstanceFollowRedirects(false); + if (connection.getResponseCode() < 200 || connection.getResponseCode() >= 400) + throw new IOException("Error " + connection.getResponseCode() + " at " + url); + return connection.getHeaderField("location"); + } +} diff --git a/src/main/java/com/github/franckyi/cmpdl/task/DownloadModpackTask.java b/src/main/java/com/github/franckyi/cmpdl/task/DownloadModpackTask.java new file mode 100644 index 0000000..0244533 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/DownloadModpackTask.java @@ -0,0 +1,53 @@ +package com.github.franckyi.cmpdl.task; + +import com.github.franckyi.cmpdl.CMPDL; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +public class DownloadModpackTask extends CustomTask { + + private URL url; + private File tmp; + + public DownloadModpackTask(String url) throws IOException { + this.url = new URL(url); + new File(CMPDL.getTempDirectory()).mkdirs(); + this.tmp = new File(CMPDL.getZipFile()); + this.tmp.createNewFile(); + } + + @Override + protected Void call0() throws Exception { + log("> Downloading modpack at " + url); + this.updateProgress(0, 1); + HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + long completeFileSize = httpConnection.getContentLength(); + BufferedInputStream bis = new BufferedInputStream(httpConnection.getInputStream()); + FileOutputStream fis = new FileOutputStream(tmp); + byte[] buffer = new byte[1024]; + long dl = 0; + int count; + while ((count = bis.read(buffer, 0, 1024)) != -1 && !isCancelled()) { + fis.write(buffer, 0, count); + dl += count; + this.updateProgress(dl, completeFileSize); + } + fis.close(); + bis.close(); + return null; + } + + @Override + protected void succeeded() { + UnzipModpackTask task = new UnzipModpackTask(tmp); + getController().setSecondaryProgress(task, "Unzipping modpack"); + new Thread(task).start(); + log("> Download succeeded !"); + } + +} diff --git a/src/main/java/com/github/franckyi/cmpdl/task/DownloadModsTask.java b/src/main/java/com/github/franckyi/cmpdl/task/DownloadModsTask.java new file mode 100644 index 0000000..d3687ce --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/DownloadModsTask.java @@ -0,0 +1,93 @@ +package com.github.franckyi.cmpdl.task; + +import com.github.franckyi.cmpdl.CMPDL; +import com.github.franckyi.cmpdl.ManifestJson; +import javafx.application.Platform; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class DownloadModsTask extends CustomTask { + + private final File folder; + private ManifestJson manifest; + + DownloadModsTask(File folder) { + new File(CMPDL.getModsDirectory()).mkdirs(); + this.folder = folder; + } + + @Override + protected Void call0() throws Exception { + updateProgress(0, 1); + manifest = CMPDL.gson.fromJson(new FileReader(CMPDL.getManifestFile()), ManifestJson.class); + int length = manifest.files.size(); + log("### " + manifest.name + " v" + manifest.version + " by " + manifest.author + " ###"); + log("> Mods download started : " + length + " files found"); + final int[] i = {0}; + for (ManifestJson.FileJson fileJson : manifest.files) { + if (!isCancelled()) { + log("> > Resolving " + fileJson.projectID + ":" + fileJson.fileID + " (" + (i[0]++) + "/" + length + ")"); + DownloadModTask task = new DownloadModTask(fileJson); + task.setOnSucceeded(event -> updateProgress(i[0] - 1, length)); + new Thread(task).start(); + while (!task.isDone() && !task.isCancelled()) { + if (task.isCancelled()) return null; + } + } else return null; + } + return null; + } + + @Override + protected void succeeded() { + log("> Mods download succeeded !"); + CopyOverridesTask task = new CopyOverridesTask(manifest, folder); + getController().setPrimaryProgress(task, "Step 3/3 : Copying overrides"); + new Thread(task).start(); + } + + private class DownloadModTask extends CustomTask { + + private final ManifestJson.FileJson fileJson; + + DownloadModTask(ManifestJson.FileJson fileJson) { + this.fileJson = fileJson; + } + + @Override + protected Void call0() throws Exception { + updateProgress(0, 1); + String url0 = crawl(crawlAddHost("http://minecraft.curseforge.com/projects/" + fileJson.projectID) + "/files/" + fileJson.fileID + "/download"); + if (DownloadModsTask.this.isCancelled()) return null; + URL url = new URL(url0); + String fileName = new File(url.getFile()).getName().replaceAll("%20", " "); + log("> > > Downloading " + fileName); + Platform.runLater(() -> getController().setSecondaryProgress(this, fileName)); + HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + long completeFileSize = httpConnection.getContentLength(); + BufferedInputStream bis = new BufferedInputStream(httpConnection.getInputStream()); + File file = new File(CMPDL.getModsDirectory() + File.separator + fileName); + file.createNewFile(); + FileOutputStream fos = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + long dl = 0; + int count; + while ((count = bis.read(buffer, 0, 1024)) != -1 && !isCancelled()) { + fos.write(buffer, 0, count); + dl += count; + this.updateProgress(dl, completeFileSize); + } + fos.close(); + bis.close(); + log("> > > Download succeeded !"); + return null; + } + + } + +} diff --git a/src/main/java/com/github/franckyi/cmpdl/task/UnzipModpackTask.java b/src/main/java/com/github/franckyi/cmpdl/task/UnzipModpackTask.java new file mode 100644 index 0000000..68edfcb --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/UnzipModpackTask.java @@ -0,0 +1,61 @@ +package com.github.franckyi.cmpdl.task; + +import java.io.*; +import java.nio.channels.FileChannel; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class UnzipModpackTask extends CustomTask { + + private final File zipfile; + private final File folder; + + UnzipModpackTask(File file) { + this.zipfile = file; + this.folder = file.getParentFile(); + } + + @Override + protected Void call0() throws Exception { + log("> Unzipping file at " + folder.getAbsolutePath()); + FileInputStream is = new FileInputStream(zipfile.getCanonicalFile()); + FileChannel channel = is.getChannel(); + ZipEntry ze; + try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is))) { + while ((ze = zis.getNextEntry()) != null && !isCancelled()) { + File f = new File(folder.getCanonicalPath(), ze.getName()); + if (ze.isDirectory()) { + f.mkdirs(); + continue; + } + f.getParentFile().mkdirs(); + try { + try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(f))) { + final byte[] buf = new byte[1024]; + int bytesRead; + long nread = 0L; + long length = zipfile.length(); + while (-1 != (bytesRead = zis.read(buf)) && !isCancelled()) { + fos.write(buf, 0, bytesRead); + nread += bytesRead; + updateProgress(channel.position(), length); + } + } + } catch (final IOException ioe) { + f.delete(); + throw ioe; + } + } + } + updateProgress(0, 0); + return null; + } + + @Override + protected void succeeded() { + DownloadModsTask task = new DownloadModsTask(folder); + getController().setPrimaryProgress(task, "Step 2/3 : Downloading dependencies"); + new Thread(task).start(); + log("> Unzipping succeeded !"); + } +} diff --git a/src/main/java/com/github/franckyi/cmpdl/task/VerifyProjectTask.java b/src/main/java/com/github/franckyi/cmpdl/task/VerifyProjectTask.java new file mode 100644 index 0000000..f864e55 --- /dev/null +++ b/src/main/java/com/github/franckyi/cmpdl/task/VerifyProjectTask.java @@ -0,0 +1,56 @@ +package com.github.franckyi.cmpdl.task; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Optional; + +public class VerifyProjectTask extends CustomTask> { + + private String fileID; + private final String modpackURL; + + public VerifyProjectTask(String fileID, String modpackURL) { + this.fileID = fileID; + this.modpackURL = modpackURL; + } + + @Override + protected Optional call0() throws Exception { + log("Verifying " + modpackURL + ":" + fileID); + String protocol, host, folder; + boolean protocolValid, hostValid, folderValid, isCurse; + URL url; + try { + url = new URL(modpackURL); + } catch (MalformedURLException e) { + log("The URL is malformed. This will not work !"); + return Optional.empty(); + } + protocol = url.getProtocol(); + host = url.getHost(); + folder = url.getFile(); + isCurse = host.equals("mods.curse.com"); + protocolValid = Arrays.asList("http", "https").contains(protocol); + hostValid = Arrays.asList("mods.curse.com", "minecraft.curseforge.com", "www.feed-the-beast.com").contains(host); + folderValid = isCurse ? folder.startsWith("/modpacks/minecraft/") && folder.split("/").length == 4 : folder.startsWith("/projects/") && folder.split("/").length == 3; + if (!protocolValid || !hostValid || !folderValid) { + log("The URL is incorrect !" + (!protocolValid ? "\n- Protocol invalid" : "") + (!hostValid ? "\n- Host invalid" : "") + (!folderValid ? "\n- Folder invalid" : "")); + return Optional.empty(); + } + if (fileID.isEmpty()) { + fileID = "latest"; + } + if (host.equals("www.feed-the-beast.com") && fileID.equals("latest")) { + log("You must specify a file ID other than\n'latest' for host www.feed-the-beast.com."); + return Optional.empty(); + } + try { + return Optional.of(crawl(isCurse ? modpackURL + "/" + (fileID.equals("latest") ? "download" : fileID) : modpackURL + "/files/" + fileID + (fileID.equals("latest") ? "" : "/download"))); + } catch (IOException e) { + log("The file doesn't exist or website is unreachable !"); + return Optional.empty(); + } + } +} diff --git a/src/main/resources/interface.fxml b/src/main/resources/interface.fxml new file mode 100644 index 0000000..be48f96 --- /dev/null +++ b/src/main/resources/interface.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + +