diff --git a/pom.xml b/pom.xml index f2bd94f..04f2daa 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ org.jetbrains.kotlin kotlin-stdlib ${kotlin.version} + compile @@ -79,6 +80,28 @@ + + org.apache.maven.plugins + maven-assembly-plugin + + jar-with-dependencies + + + false + com.minecraft.moonlake.optifinecrawler.Main + + + + + + mark-assembly + package + + single + + + + diff --git a/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/OptifineCrawler.kt b/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/OptifineCrawler.kt index 5708cf1..f79708a 100644 --- a/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/OptifineCrawler.kt +++ b/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/OptifineCrawler.kt @@ -49,6 +49,7 @@ open class OptifineCrawler { * **************************************************************************/ + constructor(factory: HttpRequestFactory = optifineFactory()): this("http://optifine.net", factory) constructor(url: String = "http://optifine.net", factory: HttpRequestFactory = optifineFactory()) { this.url = url this.factory = factory @@ -104,6 +105,14 @@ open class OptifineCrawler { */ @Throws(RuntimeException::class, IllegalStateException::class) open fun downloadOptifine(optifineVer: OptifineVersion, out: File) { + factory.requestDownload(parseResultFileUrl(optifineVer), out) + } + + /** + * 解析目标 Optifine 版本的结果文件链接 + */ + @Throws(RuntimeException::class, IllegalStateException::class) + open fun parseResultFileUrl(optifineVer: OptifineVersion): String { if(optifineVer.isEmpty() || optifineVer.downloadMirror == null) throw IllegalStateException("目标 Optifine 版本对象信息为空或下载镜像为 null 值.") @@ -118,8 +127,7 @@ open class OptifineCrawler { val matcher = Pattern.compile("\"downloadx\\?f=${if(optifineVer.preview) "preview_" else ""}OptiFine(.*)\"").matcher(content) while (matcher.find()) target = "http://optifine.net/downloadx?f=${if(optifineVer.preview) "preview_" else ""}OptiFine${matcher.group(1)}" - // 解析成功后完成最后的下载任务 - factory.requestDownload(target, out) + return target } catch (e: Exception) { throw RuntimeException(e) } @@ -141,7 +149,7 @@ open class OptifineCrawler { /** * OptifineCrawler 的默认 Http 请求工厂 */ - private fun optifineFactory(): HttpRequestFactory { + fun optifineFactory(): HttpRequestFactory { return object: HttpRequestFactory { @Throws(Exception::class) override fun requestGet(url: String): String { diff --git a/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/gui/OptifineCrawlerGui.kt b/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/gui/OptifineCrawlerGui.kt index fa0398b..982e767 100644 --- a/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/gui/OptifineCrawlerGui.kt +++ b/src/main/kotlin/com/minecraft/moonlake/optifinecrawler/gui/OptifineCrawlerGui.kt @@ -17,20 +17,32 @@ package com.minecraft.moonlake.optifinecrawler.gui +import com.minecraft.moonlake.optifinecrawler.HttpRequestFactory import com.minecraft.moonlake.optifinecrawler.OptifineCrawler import com.minecraft.moonlake.optifinecrawler.OptifineVersion import javafx.application.Application +import javafx.application.Platform import javafx.beans.binding.Bindings +import javafx.concurrent.ScheduledService +import javafx.concurrent.Task +import javafx.concurrent.Worker import javafx.geometry.Insets import javafx.scene.Parent import javafx.scene.Scene -import javafx.scene.control.Button -import javafx.scene.control.TableColumn -import javafx.scene.control.TableView +import javafx.scene.control.* import javafx.scene.layout.BorderPane import javafx.scene.layout.HBox import javafx.stage.Stage import javafx.util.Callback +import javafx.util.Duration +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.RandomAccessFile +import java.net.HttpURLConnection +import java.net.URL +import java.text.DecimalFormat +import java.util.* import java.util.concurrent.Callable class OptifineCrawlerGui: Application() { @@ -57,7 +69,8 @@ class OptifineCrawlerGui: Application() { val author = "Month_Light" val title = "OptifineCrawler $version by $author - https://github.com/lgou2w/OptifineCrawler" - val optifineCrawler = OptifineCrawler() + // private member + private val optifineCrawler = OptifineCrawler(GuiRequestFactory()) // 使用 GUI 的请求工厂 /************************************************************************** * @@ -84,8 +97,13 @@ class OptifineCrawlerGui: Application() { private val tablePreview = TableColumn("Preview") private val tableVersion = TableColumn("Version") private val tableDate = TableColumn("Date") - private val requestVerList = Button("Get Version List") + private val downloadSelected = Button("> Download Selected Item <") + private val requestVerList = Button("> Get The Version List <") + private val downloadLabel = Label("No Download") + private val downloadProgress = ProgressBar() + private val downloadGroup = HBox() private val rootPane = BorderPane() + private val btnGroup = HBox() /************************************************************************** * @@ -94,13 +112,22 @@ class OptifineCrawlerGui: Application() { **************************************************************************/ private fun createUserGui(): Parent { - val btnGroup = HBox(10.0, requestVerList) + btnGroup.spacing = 10.0 btnGroup.padding = Insets(10.0, .0, 10.0, 10.0) + downloadGroup.spacing = 10.0 + downloadProgress.setPrefSize(120.0, downloadProgress.prefHeight) + downloadGroup.padding = Insets(2.5, .0, 2.5, 5.0) + downloadGroup.children.setAll(downloadLabel, downloadProgress) + btnGroup.children.setAll(requestVerList, downloadSelected, downloadGroup) tableDownload.cellValueFactory = Callback { it -> Bindings.createStringBinding(Callable { it.value.downloadMirror }) } tablePreview.cellValueFactory = Callback { it -> Bindings.createBooleanBinding(Callable { it.value.preview }) } tableVersion.cellValueFactory = Callback { it -> Bindings.createStringBinding(Callable { it.value.version }) } tableDate.cellValueFactory = Callback { it -> Bindings.createStringBinding(Callable { it.value.date }) } tableView.columns.addAll(tableVersion, tableDate, tableDownload, tablePreview) + tableDownload.prefWidth = 250.0 + tablePreview.prefWidth = 80.0 + tableVersion.prefWidth = 200.0 + tableDate.prefWidth = 80.0 rootPane.center = tableView rootPane.bottom = btnGroup initListener() @@ -111,6 +138,176 @@ class OptifineCrawlerGui: Application() { requestVerList.setOnAction { _ -> run { val verList = optifineCrawler.requestVersionList() tableView.items.setAll(verList) + println("Optifine Crawler Version List Size: ${verList.size}") }} + downloadSelected.setOnAction { _ -> run { + val index = tableView.selectionModel.selectedIndex + if(index == -1) { + showMessage(Alert.AlertType.WARNING, "Please check the download and try again.", "Error:", ButtonType.OK) + } else { + val item = tableView.items[index] + println("Start Download: " + item.version) + downloadVer(item) + } + }} + } + + private fun downloadVer(optifineVer: OptifineVersion, disableBtnGroup: Boolean = true) { + val finalFile = File(System.getProperty("user.dir"), "${optifineVer.version}.jar") + val task = (optifineCrawler.factory as GuiRequestFactory).requestDownloadTask(optifineVer, finalFile) + task.stateProperty().addListener { _, _, newValue -> run { + when(newValue) { + Worker.State.SCHEDULED -> disableBtnGroupButton(disableBtnGroup) + Worker.State.CANCELLED, Worker.State.FAILED, Worker.State.SUCCEEDED -> releaseDownloadGroup() + else -> { } + } + }} + downloadProgress.progressProperty().bind(task.progressProperty()) + downloadLabel.textProperty().bind(task.messageProperty()) + Thread(task).start() + } + + private fun releaseDownloadGroup() { + val service = object: ScheduledService() { + override fun createTask(): Task { + return object: Task() { + override fun call() { + Platform.runLater { + disableBtnGroupButton(false) + downloadProgress.progressProperty().unbind() + downloadProgress.progress = -1.0 + downloadLabel.textProperty().unbind() + downloadLabel.text = "No Download" + } + } + } + } + } + service.delay = Duration.seconds(3.0) + service.start() + } + + private fun disableBtnGroupButton(state: Boolean) { + btnGroup.children.filter { it is Button }.forEach { it.isDisable = state } + } + + private fun showMessage(alertType: Alert.AlertType, message: String, title: String, vararg buttons: ButtonType): Optional { + val alert = Alert(alertType, message, *buttons) + alert.title = title + alert.graphic = null + alert.headerText = null + return alert.showAndWait() + } + + private inner class GuiRequestFactory: HttpRequestFactory { + // 持有一个默认的请求工厂 + private val defOptifineFactory = OptifineCrawler.optifineFactory() + private val rounding = DecimalFormat("#.00") + + @Throws(Exception::class) + override fun requestGet(url: String): String { + return defOptifineFactory.requestGet(url) + } + + @Throws(Exception::class) + override fun requestDownload(url: String, out: File) { + defOptifineFactory.requestDownload(url, out) + } + + fun requestDownloadTask(optifineVer: OptifineVersion, out: File): Task { + // 这个函数的话就是通过自己实现返回一个 task 对象 + // 拥有进度属性和消息属性 + return object: Task() { + override fun call() { + updateMessage("Downloading...") + val url = optifineCrawler.parseResultFileUrl(optifineVer) + var input: InputStream? = null + var access: RandomAccessFile? = null + try { + val connection = URL(url).openConnection() as HttpURLConnection + connection.doInput = true + connection.connectTimeout = 15000 + connection.readTimeout = 15000 + connection.addRequestProperty("User-Agent", "MoonLake OptifineCrawler by lgou2w") + connection.connect() + if(connection.responseCode / 100 != 2) + throw IOException("请求的目标响应码不为 200, 当前: ${connection.responseCode}.") + val contentLength = connection.contentLength.toLong() + if(contentLength < 1) + throw IOException("请求的目标内容长度无效.") + if(!(out.parentFile.isDirectory || out.mkdirs())) + throw IOException("无法创建目录文件.") + val tmp = File(out.absolutePath + ".mldltmp") + if(!tmp.exists()) + tmp.createNewFile() + else if(!tmp.renameTo(tmp)) + throw IllegalStateException("临时文件处于锁状态, 请检测是否被其他进程占用.") + input = connection.inputStream + access = RandomAccessFile(tmp, "rw") + val buffer: ByteArray = ByteArray(1024) + var length = 0; var downloaded = 0L + var lastDownloaded = 0L + var lastTime = System.currentTimeMillis() + while (input.read(buffer).apply { length = this } != -1) { + access.write(buffer, 0, length) + downloaded += length + val now = System.currentTimeMillis() + if(now - lastTime >= 1000) { + updateProgress(downloaded, contentLength) + updateMessage(formatDownloadSpeed(downloaded, lastDownloaded)) + lastDownloaded = downloaded + lastTime = now + } + } + if(input != null) try { + input.close() + input = null + } catch (e: Exception) { + } + if(access != null) try { + access.close() + access = null + } catch (e: Exception) { + } + if(out.exists()) + out.delete() + tmp.renameTo(out) + } catch (e: Exception) { + out.delete() + throw e + } finally { + if(input != null) try { + input.close() + } catch (e: Exception) { + } + if(access != null) try { + access.close() + } catch (e: Exception) { + } + } + } + + override fun succeeded() { + updateProgress(1.0, 1.0) + updateMessage("Download completed.") + } + + override fun failed() { + updateProgress(.0, 1.0) + updateMessage("Download failed.") + } + + override fun done() { + println("Task Done.") + } + + private fun formatDownloadSpeed(downloaded: Long, lastDownloaded: Long): String { + val kilobyte = (downloaded - lastDownloaded) / 1024.0 + if(kilobyte <= 1024.0) + return "Speed: ${rounding.format(kilobyte)}KB/s" + return "Speed: ${rounding.format(kilobyte / 1024.0)}MB/s" + } + } + } } }