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"
+ }
+ }
+ }
}
}