打包漫画图片

master
pan 4 years ago
parent 93bf0c9730
commit b5fc1bd200
  1. 32
      build.gradle.kts
  2. 6
      src/commonMain/kotlin/Api.kt
  3. 78
      src/commonMain/kotlin/Data.kt
  4. 24
      src/jsMain/kotlin/client.kt
  5. 157
      src/jsMain/kotlin/image.kt
  6. 225
      src/jsMain/kotlin/welcome.kt
  7. 67
      src/jsTest/kotlin/Test.kt
  8. 4
      src/jvmMain/kotlin/Data.kt
  9. 2
      src/jvmMain/kotlin/plugins/HTTP.kt
  10. 4
      src/jvmMain/kotlin/plugins/Monitoring.kt
  11. 343
      src/jvmMain/kotlin/plugins/Routing.kt
  12. 2
      src/jvmMain/kotlin/plugins/Serialization.kt
  13. 18
      src/jvmMain/kotlin/server.kt
  14. 3
      src/jvmMain/resources/application.conf
  15. 0
      src/jvmMain/resources/static/zip/.gitkeep
  16. 75
      src/jvmTest/kotlin/ApplicationTest.kt

@ -1,5 +1,4 @@
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
val kotlin_version="1.5.20" val kotlin_version="1.5.20"
@ -18,7 +17,6 @@ group = "jp.co.takeshobo"
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"
repositories { repositories {
jcenter()
mavenCentral() mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") } 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") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") }
@ -27,14 +25,17 @@ repositories {
kotlin { kotlin {
jvm { jvm {
compilations.all { compilations.all {
kotlinOptions.jvmTarget = "1.8" kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn"
}
} }
testRuns["test"].executionTask.configure { testRuns["test"].executionTask.configure {
useJUnit() useJUnit()
} }
withJava() withJava()
} }
js(LEGACY) { js(IR) {
binaries.executable() binaries.executable()
browser { browser {
commonWebpackConfig { commonWebpackConfig {
@ -46,12 +47,17 @@ kotlin {
} }
} }
} }
compilations.all {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn"
}
}
} }
sourceSets { sourceSets {
val commonMain by getting{ val commonMain by getting{
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version")
// implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
} }
} }
val commonTest by getting { val commonTest by getting {
@ -67,7 +73,6 @@ kotlin {
implementation("io.ktor:ktor-html-builder:$ktor_version") implementation("io.ktor:ktor-html-builder:$ktor_version")
implementation("io.ktor:ktor-serialization:$ktor_version") implementation("io.ktor:ktor-serialization:$ktor_version")
implementation("io.ktor:ktor-websockets:$ktor_version") implementation("io.ktor:ktor-websockets:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("ch.qos.logback:logback-classic:$logback_version")
} }
@ -75,6 +80,7 @@ kotlin {
val jvmTest by getting{ val jvmTest by getting{
dependencies { dependencies {
implementation("io.ktor:ktor-server-tests:$ktor_version") 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:${`kotlin-react-version`}")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:${`kotlin-react-version`}") implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:${`kotlin-react-version`}")
implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:${`kotlin-styled-version`}") implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:${`kotlin-styled-version`}")
} }
} }
val jsTest by getting val jsTest by getting
@ -93,12 +100,6 @@ application{
mainClass.set("ServerKt") mainClass.set("ServerKt")
} }
tasks {
"run"(JavaExec::class) {
environment("CHROME_BIN","F:\\ChromeUpdater\\chrome.exe")
}
}
tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack") { tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack") {
outputFileName = "js.js" outputFileName = "js.js"
} }
@ -114,12 +115,7 @@ tasks.getByName<JavaExec>("run") {
classpath(tasks.getByName<Jar>("jvmJar")) classpath(tasks.getByName<Jar>("jvmJar"))
} }
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
tasks.withType<ProcessResources>{ tasks.withType<ProcessResources>{
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
exclude(".gitkeep")
} }

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

@ -1,4 +1,3 @@
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable 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<String>, val areas: List<area>? = null) data class view(val width: Int, val height: Int, val coords: List<String>, val areas: List<area>? = null)
@Serializable @Serializable
data class t(@SerialName("ptimg-version") val ptimg_version: Int, data class t(
val resources: resources, val views: List<view>) @SerialName("ptimg-version") val ptimg_version: Int,
val resources: resources, val views: List<view>
)
@Serializable @Serializable
data class ApiResponse<T>(val message:String,@Contextual val body:T?=null) data class MessageResponse(val message: String)
sealed class TestData
//漫画图片块解析结果
@Serializable @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<T>(var dataType: String, val body: T)
//漫画信息
@Serializable @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 @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漫画解析工具"

@ -1,21 +1,33 @@
import react.dom.render
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.w3c.dom.WebSocket import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event 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() { fun main() {
window.onload = { window.onload = {
val webSocket=WebSocket("ws://localhost:8080${websocketPath}") val webSocket = WebSocket("ws://localhost:8080${Api.websocketPath}")
webSocket.onopen={event: Event -> console.info("打开连接:${event}") } webSocket.onopen = { event: Event -> console.info("打开连接:${event}") }
webSocket.onclose={event: Event -> console.info("关闭连接:${event}") } webSocket.onclose = { event: Event -> console.info("关闭连接:${event}") }
webSocket.onerror={event: Event -> console.error("发生错误:${event}") } webSocket.onerror = { event: Event -> console.error("发生错误:${event}") }
window.setInterval({
webSocket.sendCommand(command = WebSocketClientCommand.Heart)
}, 60000)
render(document.getElementById("root")) { render(document.getElementById("root")) {
child(Welcome::class) { child(Welcome::class) {
attrs { attrs {
this.webSocket=webSocket this.webSocket = webSocket
} }
} }
} }
} }
} }

@ -8,16 +8,16 @@ import org.khronos.webgl.set
import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.Image import org.w3c.dom.Image
import org.w3c.dom.events.Event
import org.w3c.dom.url.URL import org.w3c.dom.url.URL
import org.w3c.fetch.RequestInit
import org.w3c.files.Blob import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag import org.w3c.files.BlobPropertyBag
import org.w3c.xhr.FormData
import kotlin.js.Promise import kotlin.js.Promise
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.round import kotlin.math.round
import org.w3c.dom.WindowOrWorkerGlobalScope
data class un(val width: Int, val height: Int) 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) val n = (Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire(i)
?: throw IllegalArgumentException("Invalid format for Image Transfer : $i")).groupValues ?: throw IllegalArgumentException("Invalid format for Image Transfer : $i")).groupValues
val r = n[1] val r = n[1]
if (r !in t.getOwnPropertyNames()) if ("_$r" !in t.getOwnPropertyNames())
throw IllegalArgumentException("resid $r not found.") throw IllegalArgumentException("resid $r not found.")
return coord( return coord(
resid = r, xsrc = n[2].toInt(10), ysrc = n[3].toInt(10), width = n[4].toInt(10), 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 //speedbinb.js?dmy=016301:formatted:8604
fun Gs():n{ fun Gs(): n {
val e= FS() val e = FS()
val u=e.views[0] val u = e.views[0]
return n(width = u.width,height = u.height,transfers = listOf(transfer(index = 0,coords = u.coords))) return n(width = u.width, height = u.height, transfers = listOf(transfer(index = 0, coords = u.coords)))
} }
} }
object ImageDataManager {
private val data = mutableListOf<ImageData>()
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<Promise<Image>>() class ImageLoader(val urlResult: UrlResult) {
private val canvasHtml = createCanvas()
private var imageHeight = 0.0
//图片解析
private val tasks = mutableListOf<Promise<Image>>()
//speedbinb.js?dmy=016301:formatted:8766 //speedbinb.js?dmy=016301:formatted:8766
private fun callback(t:un): List<n> { private fun callback(): List<n> {
d.urlResult=urlResult d.urlResult = urlResult
val n=d.Gs() val n = d.Gs()
// console.info("Gs:") // console.info("Gs:")
// console.info(n) // console.info(n)
val u=e(un=un(width = n.width,height = n.height)) val u = e(un = un(width = n.width, height = n.height))
val s=n.transfers[0].coords val s = n.transfers[0].coords
val h= mutableListOf<n>() val h = mutableListOf<n>()
repeat(u.Xs){ repeat(u.Xs) {
val r=u.Us(t=it) val r = u.Us(t = it)
// console.info("r:") // console.info("r:")
// console.info(r) // console.info(r)
val e= mutableListOf<coord>() val e = mutableListOf<coord>()
s.forEach { s.forEach { t ->
t->
// console.info("-------------------") // console.info("-------------------")
// console.info("t:") // console.info("t:")
// 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("p.Rectangle(i):")
// console.info(_i) // console.info(_i)
p.intersect(t=r,i=_i)?.let { p.intersect(t=r,i=_i)?.let {
@ -185,7 +206,7 @@ class ImageLoader(val urlResult: UrlResult){
//speedbinb.js?dmy=016301:formatted:7976 //speedbinb.js?dmy=016301:formatted:7976
private fun us(t:n,image:Image){ private fun us(t:n,image:Image){
console.info("开始绘制漫画页") // console.info("开始绘制漫画页")
val canvas:HTMLCanvasElement= createCanvas() val canvas:HTMLCanvasElement= createCanvas()
canvas.apply { canvas.apply {
width=t.width width=t.width
@ -205,15 +226,15 @@ class ImageLoader(val urlResult: UrlResult){
Promise<Image>(executor = { resolve, _ -> Promise<Image>(executor = { resolve, _ ->
canvasToBlob(t=canvas,{blob: Blob -> canvasToBlob(t=canvas,{blob: Blob ->
console.info("blob.size:${blob.size}") // console.info("blob.size:${blob.size}")
val i=URL.createObjectURL(blob=blob) val i=URL.createObjectURL(blob=blob)
console.info("加载图片dataUrl:\n${i}") console.info("加载图片dataUrl:\n${i}")
Image().apply { Image().apply {
onload={event: Event -> onload = {
imageHeight+=this.naturalHeight imageHeight += this.naturalHeight
resolve(this) resolve(this)
} }
src=i src = i
} }
}) })
}).apply { }).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 // convert base64 to raw binary data held in a string
val byteString = window.atob(dataURI.split(',')[1]) val byteString = window.atob(dataURI.split(',')[1])
@ -236,33 +257,39 @@ class ImageLoader(val urlResult: UrlResult){
_ia[it.index] = it.value.code.toByte() _ia[it.index] = it.value.code.toByte()
} }
val dataView = DataView(arrayBuffer); val dataView = DataView(arrayBuffer)
val blob = Blob(arrayOf(dataView),options = BlobPropertyBag(type = mimeString)); return Blob(arrayOf(dataView), options = BlobPropertyBag(type = mimeString))
return blob
} }
private fun create(){
private fun create(resolve: (value: Boolean) -> Unit) {
Promise.all(promise = tasks.toTypedArray()).then { Promise.all(promise = tasks.toTypedArray()).then {
canvasHtml.let { canvasHtml.let { canvas ->
canvas-> canvas.width = it.first().naturalWidth
canvas.width=it.first().naturalWidth canvas.height = imageHeight.toInt()
canvas.height=imageHeight.toInt() val ctx: CanvasRenderingContext2D = canvas.getContext("2d") as CanvasRenderingContext2D
val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble() - 4 * 2)
ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble()) var dy = 0.0
var dy=0.0 it.forEach {
it.forEach { ctx.drawImage(
ctx.drawImage(image=it,dx=0.0,dy=dy, image = it, dx = 0.0, dy = dy,
dw=it.naturalWidth.toDouble(),dh=it.naturalHeight.toDouble().apply { dw = it.naturalWidth.toDouble(), dh = it.naturalHeight.toDouble().apply {
dy+=this.toInt()-1 dy += this.toInt() - 4
}) })
} }
Image().apply { Image().apply {
onload={ onload = {
console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}") console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}")
} }
src=canvas.toDataURL().let { URL.createObjectURL(dataURItoBlob(it)) }.apply { src = canvas.toDataURL().let {
console.info("拼接图片url:\n$this") 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( private fun canvasToBlob(
t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){ t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){
val i=t.toDataURL(type=n,quality = r).split(",")[1] 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) val blobTransfer=w(t=i)
// console.info("w[i]:") // console.info("w[i]:")
// console.info(blobTransfer) // console.info(blobTransfer)
val blob= Blob(blobParts=arrayOf(blobTransfer),options = BlobPropertyBag(type = n)) val blob= Blob(blobParts=arrayOf(blobTransfer),options = BlobPropertyBag(type = n))
console.info("blob size:${blob.size}") // console.info("blob size:${blob.size}")
callback(blob) callback(blob)
} }
@ -314,17 +341,17 @@ class ImageLoader(val urlResult: UrlResult){
} }
//speedbinb.js?dmy=016301:formatted:8020 //speedbinb.js?dmy=016301:formatted:8020
fun rebuild(){ fun rebuild(): Promise<Boolean> {
hs().then { return Promise(executor = { resolve, _ ->
console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})") hs().then {
val f=un(width = it.naturalWidth,height = it.naturalHeight) console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})")
callback(t=f).map { callback().map { t ->
t-> // val n=t(width = t.width,height = t.height)
// val n=t(width = t.width,height = t.height) us(t = t, image = it)
us(t=t,image=it) }
create(resolve = resolve)
} }
create() })
}
} }
//speedbinb.js?dmy=016301:formatted:7949 //speedbinb.js?dmy=016301:formatted:7949

@ -1,59 +1,135 @@
import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.css.* import kotlinx.css.*
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import org.w3c.fetch.RequestInit import org.w3c.fetch.RequestInit
import org.w3c.xhr.FormData import org.w3c.xhr.FormData
import react.RBuilder import react.*
import react.RComponent
import react.RProps
import react.RState
import react.dom.* import react.dom.*
import styled.css import styled.*
import styled.styledDiv
import styled.styledH1
import styled.styledInput
external interface WelcomeProps : RProps { 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) data class WelcomeState(
fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits) 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) @OptIn(ExperimentalJsExport::class)
@JsExport @JsExport
class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) { class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) {
init { init {
state=WelcomeState() state = WelcomeState()
props.webSocket.onmessage={messageEvent: MessageEvent -> props.webSocket.apply {
when(val data=messageEvent.data){ onmessage = { messageEvent: MessageEvent ->
is String-> { when (val data = messageEvent.data) {
if(data.contains("ptimg-version")){ is String -> {
val urlResult=Json.decodeFromString<UrlResult>(data) try {
console.info("ptimg_version:${urlResult.t.ptimg_version}") val res = globalJson.decodeFromString<WebSocketServerType>(data)
ImageLoader(urlResult = urlResult).rebuild() when (res.dataType) {
}else{ WebSocketResType.Heart.name -> {
val task=Json.decodeFromString<ParseTask>(data) console.info("响应心跳")
state.result="解析进度:${task.percentage}%" }
state.allowInput=(state.percentage==100F) WebSocketResType.Text.name -> {
setState(state) globalJson.decodeFromString<WebSocketServer<String>>(data).apply {
console.info("message:${body}")
setState {
result = body
}
}
}
WebSocketResType.Task.name -> {
globalJson.decodeFromString<WebSocketServer<ParseTask>>(data).apply {
setState {
percentage = body.percentage
result =
"${body.total}张图片,解析进度:${body.percentage}%(${body.finish}/${body.total})"
allowInput = (body.percentage == 100F)
}
}
}
WebSocketResType.Image.name -> {
globalJson.decodeFromString<WebSocketServer<UrlResult>>(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<WebSocketServer<ZipResult>>(data).apply {
console.info("返回压缩包:${body}")
setState {
allowInput = true
inputValue = ""
zipInfo = body
}
}
}
WebSocketResType.Manga.name -> {
globalJson.decodeFromString<WebSocketServer<MangaInfo>>(data).apply {
setState {
mangaInfo = body
}
}
}
WebSocketResType.Cancel.name -> {
globalJson.decodeFromString<WebSocketServer<Boolean>>(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<WelcomeProps, WelcomeState>(prop
css { css {
width = 100.pct width = 100.pct
} }
attrs{ attrs {
type=InputType.text type = InputType.text
value = state.url placeholder = "请拷贝完整的漫画阅读页地址到此处。"
value = state.inputValue
disabled = !state.allowInput disabled = !state.allowInput
onChangeFunction = { event -> onChangeFunction = { event ->
(event.target as HTMLInputElement).let { (event.target as HTMLInputElement).let {
console.info(it.value) console.info("inpiut url:${it.value}")
state.url=it.value setState {
setState( inputValue = it.value
state }
)
} }
} }
} }
} }
styledDiv { styledDiv {
css{ +"输入示例:"
visibility=Visibility.hidden styledUl {
height=0.px styledLi {
paddingLeft=10.px 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 { button {
attrs { attrs {
disabled=!state.allowInput disabled = !state.allowInput
onClickFunction={ onClickFunction = {
state.allowInput=false setState {
state.result="初始化解析任务请稍等" allowInput = false
setState(state) result = "初始化解析任务"
val formData=FormData() zipInfo = null
formData.append("url",state.url) mangaInfo = null
window.fetch("/api/json", RequestInit(method = "post",body = formData))
.then {
it.text()
}.then {
console.info(it)
state.result=Json.decodeFromString<MessageResponse>(it).message
setState(state)
} }
val formData = FormData()
formData.append("url", state.inputValue)
window.fetch(Api.JSON_API, RequestInit(method = "post", body = formData))
} }
} }
+"开始解析" +"开始解析"
} }
div { 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){ if(state.percentage>0F&&state.percentage<100F){
button { button {
attrs { attrs {
onClickFunction={ onClickFunction={
props.webSocket.send("cancel") props.webSocket.sendCommand(command = WebSocketClientCommand.Cancel)
state=WelcomeState()
setState(state)
} }
} }
+"取消解析任务" +"取消解析任务"

@ -1,40 +1,35 @@
import kotlinx.css.p
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject
import org.khronos.webgl.Uint8Array import kotlin.test.Ignore
import org.khronos.webgl.set
import kotlin.test.Test import kotlin.test.Test
class JsTest { class JsTest {
@Serializable @Ignore
data class FF(@SerialName("ptimg-version") val c:String) fun testRS() {
val f = Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600")
@Test println(f?.groupValues)
fun testParse(){
val c=Json.decodeFromString<t>("{\"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)
} }
private fun createUrlResult(): UrlResult {
@Test return UrlResult(
fun testRS(){ originImagePath = "", serverImagePath = "",
t = t(
val f=Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600") ptimg_version = 6666,
println(f?.groupValues) resources = resources(i = i(src = "", width = 0, height = 0)),
views = listOf()
),
romajiTitle = "",
filename = ""
)
} }
@Test @Ignore
fun testUnitArray(){ fun testUnitArray() {
ImageLoader(UrlResult(originImagePath = "",serverImagePath = "", ImageLoader(createUrlResult())
t=t(ptimg_version = 0,resources = resources(i = i(src = "",width = 0,height = 0)),views = listOf())))
.apply { .apply {
m.forEach { m.forEach {
println("m;${it}") 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<WebSocketServer<UrlResult>>(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}")
}
}
}
}
} }

@ -0,0 +1,4 @@
import io.ktor.http.content.*
//打包图片数据
data class ImageFileData(val romajiTitle: String, val data: List<PartData.FileItem>)

@ -1,8 +1,8 @@
package cool.kirito.bili.live.server.plugins package cool.kirito.bili.live.server.plugins
import io.ktor.http.*
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.* import io.ktor.features.*
import io.ktor.http.*
fun Application.configureHTTP() { fun Application.configureHTTP() {
install(CORS) { install(CORS) {

@ -1,9 +1,9 @@
package cool.kirito.bili.live.server.plugins package cool.kirito.bili.live.server.plugins
import io.ktor.features.*
import org.slf4j.event.*
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.*
import io.ktor.request.* import io.ktor.request.*
import org.slf4j.event.Level
fun Application.configureMonitoring() { fun Application.configureMonitoring() {
install(CallLogging) { install(CallLogging) {

@ -10,9 +10,10 @@ import io.ktor.http.content.*
import io.ktor.request.* import io.ktor.request.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import io.ktor.util.*
import io.ktor.websocket.* import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.html.HTML import kotlinx.html.HTML
@ -20,87 +21,172 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File 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" const val website = "https://gammaplus.takeshobo.co.jp"
var taskChannel = Channel<ParseTask>() //漫画url
val urlResultChannel = Channel<UrlResult>() val urlChannel = Channel<String>()
val htmlChannel= Channel<UrlParam>()
var currentJob: Job?=null
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<ImageFileData>()
val webSocketChannel = Channel<TestData>()
//图片JSON解析
fun Application.parse() {
launch { launch {
while (true){ log.info("初始化解析任务")
val resHtml= htmlChannel.receive()
currentJob?.let { val uploadDir = environment.config.property("ktor.deployment.filePath").getString()
if(it.isActive) this.cancel() val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }
} ?: throw IllegalArgumentException("图片存储目录初始化失败")
launch { while (true) {
log.info("开始解析:${resHtml.url}") val urlParam = urlChannel.receive()
val client = HttpClient(CIO)
//
val titlePrefix="title>" // currentJob?.let {
val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题") // if(it.isActive) this.cancel()
val romajiPrefix="/manga" // }
val romajiTitle=Regex("$romajiPrefix/\\w+").find(resHtml.url)?.value?.replace(romajiPrefix,"")?:throw IllegalArgumentException("无法解析漫画罗马音标题")
log.info("解析漫画标题") launch {
val imageDir= File(filePath,romajiTitle).apply { if(!exists()) mkdir() } 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}") log.info("漫画图片存储到:${imageDir.absolutePath}")
Regex("data/.*.json").findAll(resHtml.html) Regex("data/.*.json").findAll(resHtml)
.apply { .apply {
withIndex() withIndex().forEach {
//TODO .forEach { log.info("开始解析:${it.value.value}")
.first().let { val urlPath = "${urlParam}/${it.value.value}"
log.info("开始解析:${it.value.value}") val jsonRes: HttpResponse = client.get(urlPath)
val urlPath = "${resHtml.url}/${it.value.value}" if (jsonRes.status == HttpStatusCode.OK) {
val jsonRes: HttpResponse = client.get(urlPath) log.info("$urlPath 请求成功")
if (jsonRes.status == HttpStatusCode.OK) { val t: t = Json.decodeFromString(jsonRes.readText())
log.info("url:${urlPath} request OK") val originImagePath = "/data/${t.resources.i.src}"
val t:t=Json.decodeFromString(jsonRes.readText()) val imageUrl = "${urlParam}${originImagePath}"
val originImagePath="/data/${t.resources.i.src}" val imgRes: HttpResponse = client.get(imageUrl)
val imageUrl="${resHtml.url}${originImagePath}" if (imgRes.status == HttpStatusCode.OK && imgRes.contentType() == ContentType.Image.JPEG) {
val imgRes:HttpResponse=client.get(imageUrl) log.info("$imageUrl 请求成功")
if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){ val filename = "${it.index + 1}.jpg"
val filename="${it.index}.jpg" val file = File(imageDir, filename).apply {
val file=File(imageDir,filename).apply { writeBytes(imgRes.readBytes())
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()}")
} }
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 { } 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() } else {
}.apply { log.warn("http code:${response.status}")
currentJob=this webSocketChannel.send(StringResult(message = "${urlParam}解析失败"))
invokeOnCompletion {
log.info("${resHtml.url}解析完成")
}
} }
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() { fun Application.configureRouting() {
routing { routing {
@ -108,65 +194,150 @@ fun Application.configureRouting() {
static(uploadDir) { static(uploadDir) {
resources(uploadDir) resources(uploadDir)
} }
val zipDir = environment.config.property("ktor.deployment.zipPath").getString()
static(zipDir) {
resources(zipDir)
}
static("/static") { static("/static") {
resources() resources()
} }
webSocket(websocketPath) { // websocketSession webSocket(Api.websocketPath) { // websocketSession
launch {
while (true) {
val urlResult = urlResultChannel.receive()
outgoing.send(Frame.Text(Json.encodeToString(urlResult)))
}
}
launch { launch {
while (true) { while (true) {
val task = taskChannel.receive() when (val data = webSocketChannel.receive()) {
outgoing.send(Frame.Text(Json.encodeToString(task))) 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) { for (frame in incoming) {
when (frame) { when (frame) {
is Frame.Text -> { is Frame.Text -> {
val text = frame.readText() val input = frame.readText()
when { try {
"cancel" == text -> { val webSocketMessage: WebSocketClient = Json.decodeFromString(input)
if (currentJob?.isActive == true) { when (webSocketMessage.command) {
currentJob?.cancel() WebSocketClientCommand.Heart.name -> {
outgoing.send(Frame.Text("当前服务器解析任务已停止")) outgoing.send(
} else { Frame.Text(
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}类型消息") else -> log.warn("无法处理${frame.frameType}类型消息")
} }
} }
} }
get("/") { get("/") {
call.respondHtml(HttpStatusCode.OK, HTML::index) call.respondHtml(HttpStatusCode.OK, HTML::index)
} }
post("/api/json") { //打包漫画图片
post(Api.IMAGE_API) {
val d = call.receiveMultipart()
d.readAllParts().apply {
var romajiTitle = ""
filterIsInstance<PartData.FormItem>().forEach {
if (it.name == "romajiTitle") romajiTitle = it.value
}
val fileList = filterIsInstance<PartData.FileItem>()
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 formParameters = call.receiveParameters()
val urlParam = formParameters["url"] ?: "" val urlParam = formParameters["url"] ?: ""
log.info("urlParam:${urlParam}") log.info("urlParam:${urlParam}")
if (urlParam.startsWith(website)) { if (urlParam.startsWith(website)) {
val client = HttpClient(CIO) launch {
val response: HttpResponse = client.get(urlParam) urlChannel.send(urlParam.let { if (it.endsWith("/")) it else "$it/" })
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 = "请求失败"))
} }
client.close() call.respond(MessageResponse(message = "初始化解析任务。。。"))
} else { } else {
call.respond(MessageResponse(message = "${urlParam}:非法漫画地址")) call.respond(MessageResponse(message = "${urlParam}:非法漫画地址"))
} }

@ -1,7 +1,7 @@
package cool.kirito.bili.live.server.plugins package cool.kirito.bili.live.server.plugins
import io.ktor.features.*
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.*
import io.ktor.serialization.* import io.ktor.serialization.*
fun Application.configureSerialization() { fun Application.configureSerialization() {

@ -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.configureMonitoring
import cool.kirito.bili.live.server.plugins.configureSerialization import cool.kirito.bili.live.server.plugins.configureSerialization
import io.ktor.application.* 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.html.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import plugins.configureWebSockets import plugins.configureWebSockets
fun HTML.index() { fun HTML.index() {

@ -10,7 +10,10 @@ ktor {
jdbcUrl = "jdbc:mysql://localhost:3306/csams?serverTimezone=Asia/Shanghai" jdbcUrl = "jdbc:mysql://localhost:3306/csams?serverTimezone=Asia/Shanghai"
driverClassName = "com.mysql.cj.jdbc.Driver" driverClassName = "com.mysql.cj.jdbc.Driver"
} }
#漫画原图存储目录
filePath = static/image filePath = static/image
#漫画图片合成打包目录
zipPath = static/zip
#免重启自动重载classes目录 #免重启自动重载classes目录
watch = [ classes ] watch = [ classes ]
} }

@ -1,14 +1,21 @@
import cool.kirito.bili.live.server.plugins.configureSerialization 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.http.*
import io.ktor.config.*
import kotlin.test.*
import io.ktor.server.testing.* import io.ktor.server.testing.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import plugins.configureWebSockets import plugins.configureWebSockets
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
class ApplicationTest { class ApplicationTest {
@ -65,14 +72,60 @@ class ApplicationTest {
} }
@Test @Test
fun testUrl(){ fun testUrl() {
println(Regex("/manga/\\w+") println(
.find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value) Regex("/manga/\\w+")
.find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value
)
} }
@Test @Test
fun testSlice(){ fun testSlice() {
val t="sdfsdfsdfsd==" val t = "sdfsdfsdfsd=="
println(t.slice(IntRange(t.length-2,t.length-1))) 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<Int> = 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))
}
}
} }
Loading…
Cancel
Save