diff --git a/build.gradle.kts b/build.gradle.kts index 0cc22a3..b8f7fe3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { val kotlin_version="1.5.20" @@ -18,7 +17,6 @@ group = "jp.co.takeshobo" version = "1.0-SNAPSHOT" repositories { - jcenter() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") } @@ -27,14 +25,17 @@ repositories { kotlin { jvm { compilations.all { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn" + } } testRuns["test"].executionTask.configure { useJUnit() } withJava() } - js(LEGACY) { + js(IR) { binaries.executable() browser { commonWebpackConfig { @@ -46,12 +47,17 @@ kotlin { } } } + compilations.all { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn" + } + } } sourceSets { val commonMain by getting{ dependencies { - implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version") +// implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") } } val commonTest by getting { @@ -67,7 +73,6 @@ kotlin { implementation("io.ktor:ktor-html-builder:$ktor_version") implementation("io.ktor:ktor-serialization:$ktor_version") implementation("io.ktor:ktor-websockets:$ktor_version") - implementation("ch.qos.logback:logback-classic:$logback_version") } @@ -75,6 +80,7 @@ kotlin { val jvmTest by getting{ dependencies { implementation("io.ktor:ktor-server-tests:$ktor_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0") } } @@ -83,6 +89,7 @@ kotlin { implementation("org.jetbrains.kotlin-wrappers:kotlin-react:${`kotlin-react-version`}") implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:${`kotlin-react-version`}") implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:${`kotlin-styled-version`}") + } } val jsTest by getting @@ -93,12 +100,6 @@ application{ mainClass.set("ServerKt") } -tasks { - "run"(JavaExec::class) { - environment("CHROME_BIN","F:\\ChromeUpdater\\chrome.exe") - } -} - tasks.getByName("jsBrowserProductionWebpack") { outputFileName = "js.js" } @@ -114,12 +115,7 @@ tasks.getByName("run") { classpath(tasks.getByName("jvmJar")) } -tasks.withType { - kotlinOptions { - freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") - } -} - tasks.withType{ duplicatesStrategy = DuplicatesStrategy.INCLUDE + exclude(".gitkeep") } \ No newline at end of file diff --git a/src/commonMain/kotlin/Api.kt b/src/commonMain/kotlin/Api.kt new file mode 100644 index 0000000..1a06a42 --- /dev/null +++ b/src/commonMain/kotlin/Api.kt @@ -0,0 +1,6 @@ +object Api { + const val prefix = "/api" + const val JSON_API = "$prefix/json" + const val IMAGE_API = "$prefix/image" + const val websocketPath = "/webSocket" +} \ No newline at end of file diff --git a/src/commonMain/kotlin/Data.kt b/src/commonMain/kotlin/Data.kt index 07475ee..933dbbd 100644 --- a/src/commonMain/kotlin/Data.kt +++ b/src/commonMain/kotlin/Data.kt @@ -1,4 +1,3 @@ -import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -15,23 +14,82 @@ data class area(val href: String, val left: Int, val top: Int, val right: Int, v data class view(val width: Int, val height: Int, val coords: List, val areas: List? = null) @Serializable -data class t(@SerialName("ptimg-version") val ptimg_version: Int, - val resources: resources, val views: List) +data class t( + @SerialName("ptimg-version") val ptimg_version: Int, + val resources: resources, val views: List +) @Serializable -data class ApiResponse(val message:String,@Contextual val body:T?=null) +data class MessageResponse(val message: String) + +sealed class TestData +//漫画图片块解析结果 @Serializable -data class MessageResponse(val message: String) +data class UrlResult( + val originImagePath: String, + val serverImagePath: String, + val t: t, + val romajiTitle: String, + val filename: String, + val isLast: Boolean = false +) : TestData() + +//漫画图片解析进度 +@Serializable +data class ParseTask(val total: Int, val finish: Int, val percentage: Float) : TestData() + +@Serializable +enum class WebSocketClientCommand { + //取消解析任务 + Cancel, + + //心跳 + Heart +} + +@Serializable +enum class WebSocketResType { + //普通消息 + Text, + + //任务进度 + Task, + + //漫画数据 + Image, + + //漫画压缩包 + Zip, + + //漫画信息 + Manga, + + //取消任务 + Cancel, + + //心跳 + Heart +} + +@Serializable +data class WebSocketClient(val command: String) + +@Serializable +data class WebSocketServerType(var dataType: String) + +@Serializable +class WebSocketServer(var dataType: String, val body: T) +//漫画信息 @Serializable -data class UrlResult(val originImagePath:String,val serverImagePath:String, val t:t) +data class MangaInfo(val title: String, val romajiTitle: String, val href: String) : TestData() +//漫画打包信息 @Serializable -data class ParseTask(val total:Int,val finish:Int,val percentage:Float) +data class ZipResult(val zipUrl: String, val name: String, val size: String) : TestData() -data class UrlParam(val url:String,val html:String) +data class StringResult(val message: String) : TestData() -const val websocketPath="/webSocket" +const val websiteTitle = "朴实无华的takeshobo漫画解析工具" -const val websiteTitle="朴实无华的takeshobo漫画解析工具" \ No newline at end of file diff --git a/src/jsMain/kotlin/client.kt b/src/jsMain/kotlin/client.kt index 544c1fa..8ac7216 100644 --- a/src/jsMain/kotlin/client.kt +++ b/src/jsMain/kotlin/client.kt @@ -1,21 +1,33 @@ -import react.dom.render import kotlinx.browser.document import kotlinx.browser.window +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.w3c.dom.WebSocket import org.w3c.dom.events.Event +import react.dom.render + +val globalJson = Json { ignoreUnknownKeys = true } + +fun WebSocket.sendCommand(command: WebSocketClientCommand) { + send(globalJson.encodeToString(WebSocketClient(command = command.name))) +} fun main() { window.onload = { - val webSocket=WebSocket("ws://localhost:8080${websocketPath}") - webSocket.onopen={event: Event -> console.info("打开连接:${event}") } - webSocket.onclose={event: Event -> console.info("关闭连接:${event}") } - webSocket.onerror={event: Event -> console.error("发生错误:${event}") } + val webSocket = WebSocket("ws://localhost:8080${Api.websocketPath}") + webSocket.onopen = { event: Event -> console.info("打开连接:${event}") } + webSocket.onclose = { event: Event -> console.info("关闭连接:${event}") } + webSocket.onerror = { event: Event -> console.error("发生错误:${event}") } + window.setInterval({ + webSocket.sendCommand(command = WebSocketClientCommand.Heart) + }, 60000) render(document.getElementById("root")) { child(Welcome::class) { attrs { - this.webSocket=webSocket + this.webSocket = webSocket } } } } } + diff --git a/src/jsMain/kotlin/image.kt b/src/jsMain/kotlin/image.kt index d6604d5..580f5d1 100644 --- a/src/jsMain/kotlin/image.kt +++ b/src/jsMain/kotlin/image.kt @@ -8,16 +8,16 @@ import org.khronos.webgl.set import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.Image -import org.w3c.dom.events.Event import org.w3c.dom.url.URL +import org.w3c.fetch.RequestInit import org.w3c.files.Blob import org.w3c.files.BlobPropertyBag +import org.w3c.xhr.FormData import kotlin.js.Promise import kotlin.math.ceil import kotlin.math.max import kotlin.math.min import kotlin.math.round -import org.w3c.dom.WindowOrWorkerGlobalScope data class un(val width: Int, val height: Int) @@ -96,7 +96,7 @@ object d { val n = (Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire(i) ?: throw IllegalArgumentException("Invalid format for Image Transfer : $i")).groupValues val r = n[1] - if (r !in t.getOwnPropertyNames()) + if ("_$r" !in t.getOwnPropertyNames()) throw IllegalArgumentException("resid $r not found.") return coord( resid = r, xsrc = n[2].toInt(10), ysrc = n[3].toInt(10), width = n[4].toInt(10), @@ -117,43 +117,64 @@ object d { } //speedbinb.js?dmy=016301:formatted:8604 - fun Gs():n{ - val e= FS() - val u=e.views[0] - return n(width = u.width,height = u.height,transfers = listOf(transfer(index = 0,coords = u.coords))) + fun Gs(): n { + val e = FS() + val u = e.views[0] + return n(width = u.width, height = u.height, transfers = listOf(transfer(index = 0, coords = u.coords))) } } +object ImageDataManager { + private val data = mutableListOf() -class ImageLoader(val urlResult: UrlResult){ + fun append(d: ImageData) { + data.add(d) + } - private val canvasHtml=createCanvas() + fun requestZip(urlResult: UrlResult) { + val form = FormData() + form.apply { + append(name = "romajiTitle", value = urlResult.romajiTitle) + data.forEach { + append(name = it.fileName, value = it.blob, filename = it.fileName) + } + } + window.fetch(Api.IMAGE_API, RequestInit(method = "post", body = form)) + data.clear() + } +} - private var imageHeight=0.0 +data class ImageData(val blob: Blob, val fileName: String) - private val tasks= mutableListOf>() +class ImageLoader(val urlResult: UrlResult) { + + private val canvasHtml = createCanvas() + + private var imageHeight = 0.0 + + //图片解析 + private val tasks = mutableListOf>() //speedbinb.js?dmy=016301:formatted:8766 - private fun callback(t:un): List { - d.urlResult=urlResult - val n=d.Gs() + private fun callback(): List { + d.urlResult = urlResult + val n = d.Gs() // console.info("Gs:") // console.info(n) - val u=e(un=un(width = n.width,height = n.height)) - val s=n.transfers[0].coords - val h= mutableListOf() - repeat(u.Xs){ - val r=u.Us(t=it) + val u = e(un = un(width = n.width, height = n.height)) + val s = n.transfers[0].coords + val h = mutableListOf() + repeat(u.Xs) { + val r = u.Us(t = it) // console.info("r:") // console.info(r) - val e= mutableListOf() - s.forEach { - t-> + val e = mutableListOf() + s.forEach { t -> // console.info("-------------------") // console.info("t:") // console.info(t) - val _i=p.Rectangle(t=t.xdest,i=t.ydest,n=t.width,r=t.height) + val _i = p.Rectangle(t = t.xdest, i = t.ydest, n = t.width, r = t.height) // console.info("p.Rectangle(i):") // console.info(_i) p.intersect(t=r,i=_i)?.let { @@ -185,7 +206,7 @@ class ImageLoader(val urlResult: UrlResult){ //speedbinb.js?dmy=016301:formatted:7976 private fun us(t:n,image:Image){ - console.info("开始绘制漫画页") +// console.info("开始绘制漫画页") val canvas:HTMLCanvasElement= createCanvas() canvas.apply { width=t.width @@ -205,15 +226,15 @@ class ImageLoader(val urlResult: UrlResult){ Promise(executor = { resolve, _ -> canvasToBlob(t=canvas,{blob: Blob -> - console.info("blob.size:${blob.size}") +// console.info("blob.size:${blob.size}") val i=URL.createObjectURL(blob=blob) console.info("加载图片dataUrl:\n${i}") Image().apply { - onload={event: Event -> - imageHeight+=this.naturalHeight + onload = { + imageHeight += this.naturalHeight resolve(this) } - src=i + src = i } }) }).apply { @@ -221,7 +242,7 @@ class ImageLoader(val urlResult: UrlResult){ } } - fun dataURItoBlob(dataURI:String): Blob { + fun dataURItoBlob(dataURI: String): Blob { // convert base64 to raw binary data held in a string val byteString = window.atob(dataURI.split(',')[1]) @@ -236,33 +257,39 @@ class ImageLoader(val urlResult: UrlResult){ _ia[it.index] = it.value.code.toByte() } - val dataView = DataView(arrayBuffer); - val blob = Blob(arrayOf(dataView),options = BlobPropertyBag(type = mimeString)); - return blob + val dataView = DataView(arrayBuffer) + return Blob(arrayOf(dataView), options = BlobPropertyBag(type = mimeString)) } - private fun create(){ + + private fun create(resolve: (value: Boolean) -> Unit) { Promise.all(promise = tasks.toTypedArray()).then { - canvasHtml.let { - canvas-> - canvas.width=it.first().naturalWidth - canvas.height=imageHeight.toInt() - val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D - ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble()) - var dy=0.0 - it.forEach { - ctx.drawImage(image=it,dx=0.0,dy=dy, - dw=it.naturalWidth.toDouble(),dh=it.naturalHeight.toDouble().apply { - dy+=this.toInt()-1 - }) - } - Image().apply { - onload={ - console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}") - } - src=canvas.toDataURL().let { URL.createObjectURL(dataURItoBlob(it)) }.apply { - console.info("拼接图片url:\n$this") - } + canvasHtml.let { canvas -> + canvas.width = it.first().naturalWidth + canvas.height = imageHeight.toInt() + val ctx: CanvasRenderingContext2D = canvas.getContext("2d") as CanvasRenderingContext2D + ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble() - 4 * 2) + var dy = 0.0 + it.forEach { + ctx.drawImage( + image = it, dx = 0.0, dy = dy, + dw = it.naturalWidth.toDouble(), dh = it.naturalHeight.toDouble().apply { + dy += this.toInt() - 4 + }) + } + Image().apply { + onload = { + console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}") + } + src = canvas.toDataURL().let { + val blob = dataURItoBlob(it) + val name = "${urlResult.romajiTitle}_${urlResult.filename}" + ImageDataManager.append(ImageData(blob = blob, fileName = name)) + resolve(true) + URL.createObjectURL(blob) + }.apply { + console.info("拼接图片url:\n$this") + } } } } @@ -274,12 +301,12 @@ class ImageLoader(val urlResult: UrlResult){ private fun canvasToBlob( t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){ val i=t.toDataURL(type=n,quality = r).split(",")[1] - console.info("url length:${i.length}") +// console.info("url length:${i.length}") val blobTransfer=w(t=i) // console.info("w[i]:") // console.info(blobTransfer) val blob= Blob(blobParts=arrayOf(blobTransfer),options = BlobPropertyBag(type = n)) - console.info("blob size:${blob.size}") +// console.info("blob size:${blob.size}") callback(blob) } @@ -314,17 +341,17 @@ class ImageLoader(val urlResult: UrlResult){ } //speedbinb.js?dmy=016301:formatted:8020 - fun rebuild(){ - hs().then { - console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})") - val f=un(width = it.naturalWidth,height = it.naturalHeight) - callback(t=f).map { - t-> -// val n=t(width = t.width,height = t.height) - us(t=t,image=it) + fun rebuild(): Promise { + return Promise(executor = { resolve, _ -> + hs().then { + console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})") + callback().map { t -> + // val n=t(width = t.width,height = t.height) + us(t = t, image = it) + } + create(resolve = resolve) } - create() - } + }) } //speedbinb.js?dmy=016301:formatted:7949 diff --git a/src/jsMain/kotlin/welcome.kt b/src/jsMain/kotlin/welcome.kt index b599a0a..f1e2700 100644 --- a/src/jsMain/kotlin/welcome.kt +++ b/src/jsMain/kotlin/welcome.kt @@ -1,59 +1,135 @@ -import kotlinx.browser.document import kotlinx.browser.window import kotlinx.css.* import kotlinx.html.InputType import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onClickFunction import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import org.w3c.dom.HTMLInputElement import org.w3c.dom.MessageEvent import org.w3c.dom.WebSocket -import org.w3c.dom.events.Event import org.w3c.fetch.RequestInit import org.w3c.xhr.FormData -import react.RBuilder -import react.RComponent -import react.RProps -import react.RState +import react.* import react.dom.* -import styled.css -import styled.styledDiv -import styled.styledH1 -import styled.styledInput +import styled.* external interface WelcomeProps : RProps { - var webSocket:WebSocket + var webSocket: WebSocket } -data class WelcomeState(var url:String="",var result:String="",var percentage:kotlin.Float=0F,var allowInput:Boolean=true) : RState -fun Double.format(digits: Int): String = this.asDynamic().toFixed(digits) -fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits) +data class WelcomeState( + var inputValue: String = "", + var result: String = "", + var percentage: kotlin.Float = 0F, + var allowInput: Boolean = true, + var mangaInfo: MangaInfo? = null, + var zipInfo: ZipResult? = null +) : RState @OptIn(ExperimentalJsExport::class) @JsExport class Welcome(props: WelcomeProps) : RComponent(props) { init { - state=WelcomeState() - props.webSocket.onmessage={messageEvent: MessageEvent -> - when(val data=messageEvent.data){ - is String-> { - if(data.contains("ptimg-version")){ - val urlResult=Json.decodeFromString(data) - console.info("ptimg_version:${urlResult.t.ptimg_version}") - ImageLoader(urlResult = urlResult).rebuild() - }else{ - val task=Json.decodeFromString(data) - state.result="解析进度:${task.percentage}%" - state.allowInput=(state.percentage==100F) - setState(state) + state = WelcomeState() + props.webSocket.apply { + onmessage = { messageEvent: MessageEvent -> + when (val data = messageEvent.data) { + is String -> { + try { + val res = globalJson.decodeFromString(data) + when (res.dataType) { + WebSocketResType.Heart.name -> { + console.info("响应心跳") + } + WebSocketResType.Text.name -> { + globalJson.decodeFromString>(data).apply { + console.info("message:${body}") + setState { + result = body + } + } + + } + WebSocketResType.Task.name -> { + globalJson.decodeFromString>(data).apply { + setState { + percentage = body.percentage + result = + "共${body.total}张图片,解析进度:${body.percentage}%(${body.finish}/${body.total})" + allowInput = (body.percentage == 100F) + } + } + + } + WebSocketResType.Image.name -> { + globalJson.decodeFromString>(data).apply { + ImageLoader(urlResult = body).rebuild().then { + when { + it && body.isLast -> { + setState { + result = "打包漫画图片" + } + ImageDataManager.requestZip(urlResult = body) + } + it -> console.info("漫画地址${body.serverImagePath}解析成功") + else -> console.info("漫画地址${body.serverImagePath}解析失败") + } + } + } + + } + WebSocketResType.Zip.name -> { + globalJson.decodeFromString>(data).apply { + console.info("返回压缩包:${body}") + setState { + allowInput = true + inputValue = "" + zipInfo = body + } + } + + } + WebSocketResType.Manga.name -> { + globalJson.decodeFromString>(data).apply { + setState { + mangaInfo = body + } + } + } + WebSocketResType.Cancel.name -> { + globalJson.decodeFromString>(data).apply { + if (body) { + setState(WelcomeState()) + } + } + } + else -> { + console.warn("返回未知数据:${data}") + } + } + } catch (e: Exception) { + console.error(e) + } + } + else -> { + console.info("unknow data:${data}") } } - else->{ - console.info("unknow data:${data}") - } + } + } + + window.onbeforeunload = { + console.info("窗口即将被关闭") + if (!state.allowInput) { + val msg = "漫画解析任务正在执行,关闭窗口将自动取消当前解析任务并且无法恢复" + it.returnValue = msg + msg + } else { + props.webSocket.close() + console.info("关闭websocket") + null } } } @@ -88,65 +164,90 @@ class Welcome(props: WelcomeProps) : RComponent(prop css { width = 100.pct } - attrs{ - type=InputType.text - value = state.url + attrs { + type = InputType.text + placeholder = "请拷贝完整的漫画阅读页地址到此处。" + value = state.inputValue disabled = !state.allowInput onChangeFunction = { event -> (event.target as HTMLInputElement).let { - console.info(it.value) - state.url=it.value - setState( - state - ) + console.info("inpiut url:${it.value}") + setState { + inputValue = it.value + } } } } } styledDiv { - css{ - visibility=Visibility.hidden - height=0.px - paddingLeft=10.px + +"输入示例:" + styledUl { + styledLi { + styledA { + val href = "https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/" + attrs { + target = "_blank" + this.href = href + } + +href + } + } + } + } + styledDiv { + css { + visibility = Visibility.hidden + height = 0.px + paddingLeft = 10.px } - + state.url + +state.inputValue } } button { attrs { - disabled=!state.allowInput - onClickFunction={ - state.allowInput=false - state.result="初始化解析任务请稍等" - setState(state) - val formData=FormData() - formData.append("url",state.url) - window.fetch("/api/json", RequestInit(method = "post",body = formData)) - .then { - it.text() - }.then { - console.info(it) - state.result=Json.decodeFromString(it).message - setState(state) + disabled = !state.allowInput + onClickFunction = { + setState { + allowInput = false + result = "初始化解析任务" + zipInfo = null + mangaInfo = null } + val formData = FormData() + formData.append("url", state.inputValue) + window.fetch(Api.JSON_API, RequestInit(method = "post", body = formData)) } } +"开始解析" } div { - +state.result + state.mangaInfo?.let { + h1 { + a(href = it.href, target = "_blank") { + +"${it.title}(${it.romajiTitle})" + } + } + } + state.zipInfo?.let { + h2 { + a(href = it.zipUrl, target = "_blank") { + +"下载${it.name}(${it.size})" + } + } + } + h3 { + +state.result + } } if(state.percentage>0F&&state.percentage<100F){ button { attrs { onClickFunction={ - props.webSocket.send("cancel") - state=WelcomeState() - setState(state) + props.webSocket.sendCommand(command = WebSocketClientCommand.Cancel) } } +"取消解析任务" diff --git a/src/jsTest/kotlin/Test.kt b/src/jsTest/kotlin/Test.kt index 7461f57..ee3642e 100644 --- a/src/jsTest/kotlin/Test.kt +++ b/src/jsTest/kotlin/Test.kt @@ -1,40 +1,35 @@ -import kotlinx.css.p -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable + import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.khronos.webgl.Uint8Array -import org.khronos.webgl.set - +import kotlinx.serialization.json.jsonObject +import kotlin.test.Ignore import kotlin.test.Test class JsTest { - @Serializable - data class FF(@SerialName("ptimg-version") val c:String) - - @Test - fun testParse(){ - val c=Json.decodeFromString("{\"ptimg-version\":1,\"resources\":{\"i\":{\"src\":\"0012.jpg\",\"width\":908,\"height\":1264}},\"views\":[{\"width\":844,\"height\":1200,\"coords\":[\"i:4,4+106,150>420,1050\",\"i:118,4+106,150>530,450\",\"i:232,4+106,150>738,0\",\"i:346,4+106,150>526,750\",\"i:460,4+106,150>420,750\",\"i:574,4+106,150>106,300\",\"i:688,4+106,150>318,600\",\"i:802,4+102,150>742,600\",\"i:4,162+106,150>106,450\",\"i:118,162+106,150>526,1050\",\"i:232,162+106,150>0,900\",\"i:346,162+106,150>636,150\",\"i:460,162+102,150>424,900\",\"i:570,162+106,150>0,1050\",\"i:684,162+106,150>106,1050\",\"i:798,162+106,150>0,600\",\"i:4,320+106,150>738,1050\",\"i:118,320+106,150>106,150\",\"i:232,320+106,150>424,600\",\"i:346,320+106,150>420,300\",\"i:460,320+106,150>530,150\",\"i:574,320+106,150>424,0\",\"i:688,320+102,150>742,150\",\"i:798,320+106,150>0,450\",\"i:4,478+106,150>738,750\",\"i:118,478+106,150>632,1050\",\"i:232,478+106,150>106,900\",\"i:346,478+102,150>106,750\",\"i:456,478+106,150>106,600\",\"i:570,478+106,150>424,450\",\"i:684,478+106,150>212,900\",\"i:798,478+106,150>318,450\",\"i:4,636+106,150>632,300\",\"i:118,636+106,150>314,1050\",\"i:232,636+106,150>106,0\",\"i:346,636+106,150>318,0\",\"i:460,636+106,150>314,750\",\"i:574,636+102,150>636,0\",\"i:684,636+106,150>632,900\",\"i:798,636+106,150>738,900\",\"i:4,794+106,150>318,150\",\"i:118,794+106,150>530,600\",\"i:232,794+106,150>212,0\",\"i:346,794+106,150>212,450\",\"i:460,794+106,150>0,150\",\"i:574,794+102,150>636,450\",\"i:684,794+106,150>738,300\",\"i:798,794+106,150>0,750\",\"i:4,952+106,150>208,750\",\"i:118,952+106,150>212,150\",\"i:232,952+106,150>636,600\",\"i:346,952+106,150>212,300\",\"i:460,952+106,150>0,300\",\"i:574,952+102,150>212,1050\",\"i:684,952+106,150>526,900\",\"i:798,952+106,150>424,150\",\"i:4,1110+106,150>530,0\",\"i:118,1110+106,150>526,300\",\"i:232,1110+106,150>632,750\",\"i:346,1110+106,150>738,450\",\"i:460,1110+106,150>212,600\",\"i:574,1110+106,150>0,0\",\"i:688,1110+102,150>318,300\",\"i:798,1110+106,150>318,900\"]}]}") - println(c.ptimg_version) - println(c.resources.i) - println(c.views) + @Ignore + fun testRS() { + val f = Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600") + println(f?.groupValues) } - - @Test - fun testRS(){ - - val f=Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600") - println(f?.groupValues) + private fun createUrlResult(): UrlResult { + return UrlResult( + originImagePath = "", serverImagePath = "", + t = t( + ptimg_version = 6666, + resources = resources(i = i(src = "", width = 0, height = 0)), + views = listOf() + ), + romajiTitle = "", + filename = "" + ) } - @Test - fun testUnitArray(){ - ImageLoader(UrlResult(originImagePath = "",serverImagePath = "", - t=t(ptimg_version = 0,resources = resources(i = i(src = "",width = 0,height = 0)),views = listOf()))) + @Ignore + fun testUnitArray() { + ImageLoader(createUrlResult()) .apply { m.forEach { println("m;${it}") @@ -42,4 +37,24 @@ class JsTest { } } + @Test + fun testJSON() { + + val urlResult = createUrlResult() + + globalJson.encodeToString(WebSocketServer(dataType = WebSocketResType.Image.name, body = urlResult)).apply { + println("序列化:$this") + globalJson.decodeFromString>(this).apply { + println("反序列化:${this.body}") + } + globalJson.parseToJsonElement(this).apply { + println("type:${jsonObject.getValue("dataType")}") + globalJson.decodeFromJsonElement(WebSocketServer.serializer(UrlResult.serializer()), this).apply { + println("反序列化:${this.body}") + } + } + } + } + + } diff --git a/src/jvmMain/kotlin/Data.kt b/src/jvmMain/kotlin/Data.kt new file mode 100644 index 0000000..54550e3 --- /dev/null +++ b/src/jvmMain/kotlin/Data.kt @@ -0,0 +1,4 @@ +import io.ktor.http.content.* + +//打包图片数据 +data class ImageFileData(val romajiTitle: String, val data: List) diff --git a/src/jvmMain/kotlin/plugins/HTTP.kt b/src/jvmMain/kotlin/plugins/HTTP.kt index a276d95..7905ec1 100644 --- a/src/jvmMain/kotlin/plugins/HTTP.kt +++ b/src/jvmMain/kotlin/plugins/HTTP.kt @@ -1,8 +1,8 @@ package cool.kirito.bili.live.server.plugins -import io.ktor.http.* import io.ktor.application.* import io.ktor.features.* +import io.ktor.http.* fun Application.configureHTTP() { install(CORS) { diff --git a/src/jvmMain/kotlin/plugins/Monitoring.kt b/src/jvmMain/kotlin/plugins/Monitoring.kt index b61a1d3..c01640e 100644 --- a/src/jvmMain/kotlin/plugins/Monitoring.kt +++ b/src/jvmMain/kotlin/plugins/Monitoring.kt @@ -1,9 +1,9 @@ package cool.kirito.bili.live.server.plugins -import io.ktor.features.* -import org.slf4j.event.* import io.ktor.application.* +import io.ktor.features.* import io.ktor.request.* +import org.slf4j.event.Level fun Application.configureMonitoring() { install(CallLogging) { diff --git a/src/jvmMain/kotlin/plugins/Routing.kt b/src/jvmMain/kotlin/plugins/Routing.kt index df6b49d..af9821f 100644 --- a/src/jvmMain/kotlin/plugins/Routing.kt +++ b/src/jvmMain/kotlin/plugins/Routing.kt @@ -10,9 +10,10 @@ import io.ktor.http.content.* import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* +import io.ktor.util.* import io.ktor.websocket.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.html.HTML @@ -20,87 +21,172 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream const val website = "https://gammaplus.takeshobo.co.jp" -var taskChannel = Channel() -val urlResultChannel = Channel() -val htmlChannel= Channel() -var currentJob: Job?=null +//漫画url +val urlChannel = Channel() -fun Application.parse(){ - log.info("初始化解析任务") +//当前漫画解析任务 +var currentJob: Job? = null - val uploadDir = environment.config.property("ktor.deployment.filePath").getString() - val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }?:throw IllegalArgumentException("图片存储目录初始化失败") +//合成图片打包数据 +val imageDataChannel = Channel() +val webSocketChannel = Channel() + +//图片JSON解析 +fun Application.parse() { launch { - while (true){ - val resHtml= htmlChannel.receive() + log.info("初始化解析任务") - currentJob?.let { - if(it.isActive) this.cancel() - } + val uploadDir = environment.config.property("ktor.deployment.filePath").getString() + val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") } + ?: throw IllegalArgumentException("图片存储目录初始化失败") - launch { - log.info("开始解析:${resHtml.url}") - val client = HttpClient(CIO) - - val titlePrefix="title>" - val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题") - val romajiPrefix="/manga" - val romajiTitle=Regex("$romajiPrefix/\\w+").find(resHtml.url)?.value?.replace(romajiPrefix,"")?:throw IllegalArgumentException("无法解析漫画罗马音标题") - log.info("解析漫画标题") - val imageDir= File(filePath,romajiTitle).apply { if(!exists()) mkdir() } + while (true) { + val urlParam = urlChannel.receive() + +// +// currentJob?.let { +// if(it.isActive) this.cancel() +// } + + launch { + log.info("开始解析:${urlParam}") + val client = HttpClient(CIO) + val response: HttpResponse = client.get(urlParam) + if (response.status == HttpStatusCode.OK) { + val resHtml = response.readText() +// log.info("resHtml:\n$resHtml") + val titlePrefix = "title>" + val title = + Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml)?.value?.replace( + titlePrefix, + "" + ) ?: throw IllegalArgumentException("无法解析漫画标题") + val romajiPrefix = "/manga/" + val romajiTitle = Regex("$romajiPrefix\\w+").find(resHtml)?.value?.replace(romajiPrefix, "") + ?: throw IllegalArgumentException("无法解析漫画罗马音标题") + log.info("漫画标题title=$title,romajiTitle=$romajiTitle") + webSocketChannel.send(MangaInfo(href = urlParam, title = title, romajiTitle = romajiTitle)) + val imageDir = File(filePath, romajiTitle).apply { if (!exists()) mkdir() } log.info("漫画图片存储到:${imageDir.absolutePath}") - Regex("data/.*.json").findAll(resHtml.html) + Regex("data/.*.json").findAll(resHtml) .apply { - withIndex() -//TODO .forEach { - .first().let { - log.info("开始解析:${it.value.value}") - val urlPath = "${resHtml.url}/${it.value.value}" - val jsonRes: HttpResponse = client.get(urlPath) - if (jsonRes.status == HttpStatusCode.OK) { - log.info("url:${urlPath} request OK") - val t:t=Json.decodeFromString(jsonRes.readText()) - val originImagePath="/data/${t.resources.i.src}" - val imageUrl="${resHtml.url}${originImagePath}" - val imgRes:HttpResponse=client.get(imageUrl) - if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){ - val filename="${it.index}.jpg" - val file=File(imageDir,filename).apply { - writeBytes(imgRes.readBytes()) - } - log.info("存储漫画图片:${file.absolutePath}") - val serverImagePath="/${uploadDir}/${romajiTitle}/${filename}" - log.info("serverImagePath:${serverImagePath}") - urlResultChannel.send(UrlResult(originImagePath = originImagePath, t = t, - serverImagePath = serverImagePath)) - }else{ - log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}") + withIndex().forEach { + log.info("开始解析:${it.value.value}") + val urlPath = "${urlParam}/${it.value.value}" + val jsonRes: HttpResponse = client.get(urlPath) + if (jsonRes.status == HttpStatusCode.OK) { + log.info("$urlPath 请求成功") + val t: t = Json.decodeFromString(jsonRes.readText()) + val originImagePath = "/data/${t.resources.i.src}" + val imageUrl = "${urlParam}${originImagePath}" + val imgRes: HttpResponse = client.get(imageUrl) + if (imgRes.status == HttpStatusCode.OK && imgRes.contentType() == ContentType.Image.JPEG) { + log.info("$imageUrl 请求成功") + val filename = "${it.index + 1}.jpg" + val file = File(imageDir, filename).apply { + writeBytes(imgRes.readBytes()) } + log.info("存储漫画图片:${file.absolutePath}") + val serverImagePath = "/${uploadDir}/${romajiTitle}/${filename}" + log.info("serverImagePath:${serverImagePath}") + webSocketChannel.send( + UrlResult( + originImagePath = originImagePath, + t = t, + serverImagePath = serverImagePath, + romajiTitle = romajiTitle, + filename = filename, + isLast = count() == it.index + 1 + ) + ) } else { - log.warn("json url:${urlPath} 响应码:${jsonRes.status}") + log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}") } - taskChannel.send(ParseTask(total = count(), finish = it.index + 1,percentage = if(count()==it.index+1) 100F else String.format("%.2f",(it.index+1)*100F/count()).toFloat())) + } else { + log.warn("json url:${urlPath} 响应码:${jsonRes.status}") } + webSocketChannel.send( + ParseTask( + total = count(), + finish = it.index + 1, + percentage = if (count() == it.index + 1) 100F else String.format( + "%.2f", + (it.index + 1) * 100F / count() + ).toFloat() + ) + ) + } } - client.close() - }.apply { - currentJob=this - invokeOnCompletion { - log.info("${resHtml.url}解析完成") - } + } else { + log.warn("http code:${response.status}") + webSocketChannel.send(StringResult(message = "${urlParam}解析失败")) } + client.close() + }.apply { + currentJob = this + invokeOnCompletion { + log.info("${urlParam}解析完成") + } + } + + } + } + launch { + log.info("初始化合成图片任务") + + val zipDir = environment.config.property("ktor.deployment.zipPath").getString() + val zipPath = environment.classLoader.getResource(zipDir)?.path.apply { log.info("图片压缩目录:${this}") } + ?: throw IllegalArgumentException("图片压缩目录初始化失败") + + while (true) { + val imageFileData = imageDataChannel.receive() + File(zipPath, imageFileData.romajiTitle).apply { + imageFileData.data.apply { + launch(Dispatchers.IO) { + File(zipPath, "${imageFileData.romajiTitle}.zip").apply { + ZipOutputStream(FileOutputStream(this)).use { out -> + forEach { file -> + + out.putNextEntry( + ZipEntry( + file.originalFileName ?: throw IllegalArgumentException("无法获取图片名字") + ) + ) + out.write(file.streamProvider().readBytes()) + out.closeEntry() + } + } + webSocketChannel.send( + ZipResult( + zipUrl = "/${zipDir}/${name}", + name = name, + size = String.format("%.2fMB", length() / 1024 / 1024F) + ) + ) + webSocketChannel.send(StringResult(message = "打包任务完成")) + } + } + } + } } } } +fun createFrameText(message: String): Frame.Text { + return Frame.Text(Json.encodeToString(WebSocketServer(body = message, dataType = WebSocketResType.Text.name))) +} + fun Application.configureRouting() { routing { @@ -108,65 +194,150 @@ fun Application.configureRouting() { static(uploadDir) { resources(uploadDir) } + val zipDir = environment.config.property("ktor.deployment.zipPath").getString() + static(zipDir) { + resources(zipDir) + } static("/static") { resources() } - webSocket(websocketPath) { // websocketSession - launch { - while (true) { - val urlResult = urlResultChannel.receive() - outgoing.send(Frame.Text(Json.encodeToString(urlResult))) - } - } + webSocket(Api.websocketPath) { // websocketSession launch { while (true) { - val task = taskChannel.receive() - outgoing.send(Frame.Text(Json.encodeToString(task))) + when (val data = webSocketChannel.receive()) { + is StringResult -> outgoing.send(createFrameText(data.message)) + is UrlResult -> outgoing.send( + Frame.Text( + Json.encodeToString( + WebSocketServer( + body = data, + dataType = WebSocketResType.Image.name + ) + ) + ) + ) + is MangaInfo -> outgoing.send( + Frame.Text( + Json.encodeToString( + WebSocketServer( + body = data, + dataType = WebSocketResType.Manga.name + ) + ) + ) + ) + is ZipResult -> outgoing.send( + Frame.Text( + Json.encodeToString( + WebSocketServer( + body = data, + dataType = WebSocketResType.Zip.name + ) + ) + ) + ) + is ParseTask -> outgoing.send( + Frame.Text( + Json.encodeToString( + WebSocketServer( + body = data, + dataType = WebSocketResType.Task.name + ) + ) + ) + ) + } } } + for (frame in incoming) { when (frame) { is Frame.Text -> { - val text = frame.readText() - when { - "cancel" == text -> { - if (currentJob?.isActive == true) { - currentJob?.cancel() - outgoing.send(Frame.Text("当前服务器解析任务已停止")) - } else { - outgoing.send(Frame.Text("当前服务器没有正在运行的解析任务")) + val input = frame.readText() + try { + val webSocketMessage: WebSocketClient = Json.decodeFromString(input) + when (webSocketMessage.command) { + WebSocketClientCommand.Heart.name -> { + outgoing.send( + Frame.Text( + Json.encodeToString( + WebSocketServer(dataType = WebSocketResType.Heart.name, body = "心跳返回") + ) + ) + ) + } + WebSocketClientCommand.Cancel.name -> { + if (currentJob?.isActive == true) { + currentJob?.cancel() + outgoing.send( + Frame.Text( + Json.encodeToString( + WebSocketServer( + dataType = WebSocketResType.Cancel.name, + body = true + ) + ) + ) + ) + } else { + outgoing.send(createFrameText(message = "当前服务器没有正在运行的解析任务")) + } } + else -> outgoing.send(createFrameText(message = "未知命令:${webSocketMessage.command}")) } - "exit" == text -> close(CloseReason(CloseReason.Codes.NORMAL, "Client said exit")) + } catch (e: Exception) { + log.error(e) + outgoing.send(Frame.Text("非法命令:${input}")) } } + is Frame.Close -> { + currentJob?.cancel() + log.info("取消解析任务") + } else -> log.warn("无法处理${frame.frameType}类型消息") } } } + + get("/") { call.respondHtml(HttpStatusCode.OK, HTML::index) } - post("/api/json") { + //打包漫画图片 + post(Api.IMAGE_API) { + val d = call.receiveMultipart() + d.readAllParts().apply { + var romajiTitle = "" + filterIsInstance().forEach { + if (it.name == "romajiTitle") romajiTitle = it.value + } + val fileList = filterIsInstance() + if (romajiTitle.isNotEmpty() && fileList.isNotEmpty()) { + launch { + imageDataChannel.send(ImageFileData(romajiTitle = romajiTitle, data = fileList)) + } + call.respond(MessageResponse(message = "初始化打包任务。。。")) + } else { + log.warn("无法打包漫画图片,参数不合法[romajiTitle:${romajiTitle},fileList=${fileList}]") + call.respond(MessageResponse(message = "无法打包漫画图片,请联系管理员")) + } + } + } + + //解析漫画图片数据 + post(Api.JSON_API) { val formParameters = call.receiveParameters() val urlParam = formParameters["url"] ?: "" log.info("urlParam:${urlParam}") if (urlParam.startsWith(website)) { - val client = HttpClient(CIO) - val response: HttpResponse = client.get(urlParam) - if (response.status == HttpStatusCode.OK) { - val resHtml = response.readText() - htmlChannel.send(UrlParam(url = urlParam, html = resHtml)) - call.respond(MessageResponse(message = "开始执行解析任务")) - } else { - log.warn("http code:${response.status}") - call.respond(MessageResponse(message = "请求失败")) + launch { + urlChannel.send(urlParam.let { if (it.endsWith("/")) it else "$it/" }) } - client.close() + call.respond(MessageResponse(message = "初始化解析任务。。。")) } else { call.respond(MessageResponse(message = "${urlParam}:非法漫画地址")) } diff --git a/src/jvmMain/kotlin/plugins/Serialization.kt b/src/jvmMain/kotlin/plugins/Serialization.kt index 423b0ad..a4870d6 100644 --- a/src/jvmMain/kotlin/plugins/Serialization.kt +++ b/src/jvmMain/kotlin/plugins/Serialization.kt @@ -1,7 +1,7 @@ package cool.kirito.bili.live.server.plugins -import io.ktor.features.* import io.ktor.application.* +import io.ktor.features.* import io.ktor.serialization.* fun Application.configureSerialization() { diff --git a/src/jvmMain/kotlin/server.kt b/src/jvmMain/kotlin/server.kt index 0bcb469..b7f7126 100644 --- a/src/jvmMain/kotlin/server.kt +++ b/src/jvmMain/kotlin/server.kt @@ -2,25 +2,7 @@ import cool.kirito.bili.live.server.plugins.configureHTTP import cool.kirito.bili.live.server.plugins.configureMonitoring import cool.kirito.bili.live.server.plugins.configureSerialization import io.ktor.application.* -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.features.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.features.* -import io.ktor.html.respondHtml -import io.ktor.http.* -import io.ktor.routing.get -import io.ktor.routing.routing -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.http.content.resources -import io.ktor.http.content.static -import io.ktor.response.* -import io.ktor.serialization.* import kotlinx.html.* -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import plugins.configureWebSockets fun HTML.index() { diff --git a/src/jvmMain/resources/application.conf b/src/jvmMain/resources/application.conf index a489825..7ff0874 100644 --- a/src/jvmMain/resources/application.conf +++ b/src/jvmMain/resources/application.conf @@ -10,7 +10,10 @@ ktor { jdbcUrl = "jdbc:mysql://localhost:3306/csams?serverTimezone=Asia/Shanghai" driverClassName = "com.mysql.cj.jdbc.Driver" } + #漫画原图存储目录 filePath = static/image + #漫画图片合成打包目录 + zipPath = static/zip #免重启自动重载classes目录 watch = [ classes ] } diff --git a/src/jvmMain/resources/static/zip/.gitkeep b/src/jvmMain/resources/static/zip/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/jvmTest/kotlin/ApplicationTest.kt b/src/jvmTest/kotlin/ApplicationTest.kt index 33e9da4..c14f582 100644 --- a/src/jvmTest/kotlin/ApplicationTest.kt +++ b/src/jvmTest/kotlin/ApplicationTest.kt @@ -1,14 +1,21 @@ import cool.kirito.bili.live.server.plugins.configureSerialization +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* - -import io.ktor.config.* -import kotlin.test.* import io.ktor.server.testing.* -import kotlinx.coroutines.channels.Channel -import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import plugins.configureWebSockets +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals class ApplicationTest { @@ -65,14 +72,60 @@ class ApplicationTest { } @Test - fun testUrl(){ - println(Regex("/manga/\\w+") - .find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value) + fun testUrl() { + println( + Regex("/manga/\\w+") + .find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value + ) } @Test - fun testSlice(){ - val t="sdfsdfsdfsd==" - println(t.slice(IntRange(t.length-2,t.length-1))) + fun testSlice() { + val t = "sdfsdfsdfsd==" + println(t.slice(IntRange(t.length - 2, t.length - 1))) } + + @Test + fun testJSON() { + val input = "{\"dataType\":\"Text\",\"body\":123}" + val json = Json { ignoreUnknownKeys = true } + val f: WebSocketServerType = json.decodeFromString(input) + println("反序列化:${f}") + val we: WebSocketServer = Json.decodeFromString(input) + println("反序列化:${we}") + + val s = WebSocketServer(body = "fsdfsdf", dataType = WebSocketResType.Text.name) + println("序列化:${json.encodeToString(s)}") + + Json.encodeToString(WebSocketServer(body = "666", dataType = WebSocketResType.Text.name)).apply { + println("序列化:${this}") + } + + Json.encodeToString(WebSocketServer(body = "666", dataType = WebSocketResType.Text.name)).apply { + println(this) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testHttpClient(): Unit = runBlockingTest { + launch { + delay(5000) + } + HttpClient(CIO).apply { + val httpResponse: HttpResponse = get("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1") + println(httpResponse.status) + } + + } + + @Test + fun testSize() { + File("E:\\JetBrains\\IdeaProjects\\manga\\build\\processedResources\\jvm\\main\\static\\zip\\kobito_recipe.zip") + .apply { + println(String.format("%.2fMB", length() / 1024 / 1024F)) + } + + } + } \ No newline at end of file