打包漫画图片

master
pan 4 years ago
parent 93bf0c9730
commit b5fc1bd200
  1. 32
      build.gradle.kts
  2. 6
      src/commonMain/kotlin/Api.kt
  3. 76
      src/commonMain/kotlin/Data.kt
  4. 16
      src/jsMain/kotlin/client.kt
  5. 79
      src/jsMain/kotlin/image.kt
  6. 193
      src/jsMain/kotlin/welcome.kt
  7. 63
      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. 277
      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. 67
      src/jvmTest/kotlin/ApplicationTest.kt

@ -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<KotlinWebpack>("jsBrowserProductionWebpack") {
outputFileName = "js.js"
}
@ -114,12 +115,7 @@ tasks.getByName<JavaExec>("run") {
classpath(tasks.getByName<Jar>("jvmJar"))
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
tasks.withType<ProcessResources>{
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.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)
@Serializable
data class t(@SerialName("ptimg-version") val ptimg_version: Int,
val resources: resources, val views: List<view>)
data class t(
@SerialName("ptimg-version") val ptimg_version: Int,
val resources: resources, val views: List<view>
)
@Serializable
data class ApiResponse<T>(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 UrlResult(val originImagePath:String,val serverImagePath:String, val t:t)
data class WebSocketClient(val command: String)
@Serializable
data class ParseTask(val total:Int,val finish:Int,val percentage:Float)
data class WebSocketServerType(var dataType: String)
data class UrlParam(val url:String,val html:String)
@Serializable
class WebSocketServer<T>(var dataType: String, val body: T)
const val websocketPath="/webSocket"
//漫画信息
@Serializable
data class MangaInfo(val title: String, val romajiTitle: String, val href: String) : TestData()
//漫画打包信息
@Serializable
data class ZipResult(val zipUrl: String, val name: String, val size: String) : TestData()
data class StringResult(val message: String) : TestData()
const val websiteTitle = "朴实无华的takeshobo漫画解析工具"

@ -1,15 +1,26 @@
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}")
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 {
@ -19,3 +30,4 @@ fun main() {
}
}
}

@ -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),
@ -125,6 +125,27 @@ object d {
}
object ImageDataManager {
private val data = mutableListOf<ImageData>()
fun append(d: ImageData) {
data.add(d)
}
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()
}
}
data class ImageData(val blob: Blob, val fileName: String)
class ImageLoader(val urlResult: UrlResult) {
@ -132,10 +153,11 @@ class ImageLoader(val urlResult: UrlResult){
private var imageHeight = 0.0
//图片解析
private val tasks = mutableListOf<Promise<Image>>()
//speedbinb.js?dmy=016301:formatted:8766
private fun callback(t:un): List<n> {
private fun callback(): List<n> {
d.urlResult = urlResult
val n = d.Gs()
// console.info("Gs:")
@ -148,8 +170,7 @@ class ImageLoader(val urlResult: UrlResult){
// console.info("r:")
// console.info(r)
val e = mutableListOf<coord>()
s.forEach {
t->
s.forEach { t ->
// console.info("-------------------")
// console.info("t:")
// console.info(t)
@ -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,11 +226,11 @@ class ImageLoader(val urlResult: UrlResult){
Promise<Image>(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 ->
onload = {
imageHeight += this.naturalHeight
resolve(this)
}
@ -236,31 +257,37 @@ 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->
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())
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,
ctx.drawImage(
image = it, dx = 0.0, dy = dy,
dw = it.naturalWidth.toDouble(), dh = it.naturalHeight.toDouble().apply {
dy+=this.toInt()-1
dy += this.toInt() - 4
})
}
Image().apply {
onload = {
console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}")
}
src=canvas.toDataURL().let { URL.createObjectURL(dataURItoBlob(it)) }.apply {
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(){
fun rebuild(): Promise<Boolean> {
return Promise(executor = { resolve, _ ->
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->
callback().map { t ->
// val n=t(width = t.width,height = t.height)
us(t = t, image = it)
}
create()
create(resolve = resolve)
}
})
}
//speedbinb.js?dmy=016301:formatted:7949

@ -1,35 +1,31 @@
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
}
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
@ -37,18 +33,84 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
init {
state = WelcomeState()
props.webSocket.onmessage={messageEvent: MessageEvent ->
props.webSocket.apply {
onmessage = { messageEvent: MessageEvent ->
when (val data = messageEvent.data) {
is String -> {
if(data.contains("ptimg-version")){
val urlResult=Json.decodeFromString<UrlResult>(data)
console.info("ptimg_version:${urlResult.t.ptimg_version}")
ImageLoader(urlResult = urlResult).rebuild()
}else{
val task=Json.decodeFromString<ParseTask>(data)
state.result="解析进度:${task.percentage}%"
state.allowInput=(state.percentage==100F)
setState(state)
try {
val res = globalJson.decodeFromString<WebSocketServerType>(data)
when (res.dataType) {
WebSocketResType.Heart.name -> {
console.info("响应心跳")
}
WebSocketResType.Text.name -> {
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 -> {
@ -58,6 +120,20 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
}
}
window.onbeforeunload = {
console.info("窗口即将被关闭")
if (!state.allowInput) {
val msg = "漫画解析任务正在执行,关闭窗口将自动取消当前解析任务并且无法恢复"
it.returnValue = msg
msg
} else {
props.webSocket.close()
console.info("关闭websocket")
null
}
}
}
override fun RBuilder.render() {
styledH1 {
@ -90,27 +166,42 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
}
attrs {
type = InputType.text
value = state.url
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 {
+"输入示例:"
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
}
}
@ -118,35 +209,45 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
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<MessageResponse>(it).message
setState(state)
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.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)
}
}
+"取消解析任务"

@ -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<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)
}
@Test
@Ignore
fun testRS() {
val f = Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600")
println(f?.groupValues)
}
@Test
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 = ""
)
}
@Ignore
fun testUnitArray() {
ImageLoader(UrlResult(originImagePath = "",serverImagePath = "",
t=t(ptimg_version = 0,resources = resources(i = i(src = "",width = 0,height = 0)),views = listOf())))
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<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
import io.ktor.http.*
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
fun Application.configureHTTP() {
install(CORS) {

@ -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) {

@ -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,85 +21,170 @@ 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<ParseTask>()
val urlResultChannel = Channel<UrlResult>()
val htmlChannel= Channel<UrlParam>()
//漫画url
val urlChannel = Channel<String>()
//当前漫画解析任务
var currentJob: Job? = null
//合成图片打包数据
val imageDataChannel = Channel<ImageFileData>()
val webSocketChannel = Channel<TestData>()
//图片JSON解析
fun Application.parse() {
launch {
log.info("初始化解析任务")
val uploadDir = environment.config.property("ktor.deployment.filePath").getString()
val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }?:throw IllegalArgumentException("图片存储目录初始化失败")
val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }
?: throw IllegalArgumentException("图片存储目录初始化失败")
launch {
while (true) {
val resHtml= htmlChannel.receive()
val urlParam = urlChannel.receive()
currentJob?.let {
if(it.isActive) this.cancel()
}
//
// currentJob?.let {
// if(it.isActive) this.cancel()
// }
launch {
log.info("开始解析:${resHtml.url}")
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.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 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 {
withIndex().forEach {
log.info("开始解析:${it.value.value}")
val urlPath = "${resHtml.url}/${it.value.value}"
val urlPath = "${urlParam}/${it.value.value}"
val jsonRes: HttpResponse = client.get(urlPath)
if (jsonRes.status == HttpStatusCode.OK) {
log.info("url:${urlPath} request OK")
log.info("$urlPath 请求成功")
val t: t = Json.decodeFromString(jsonRes.readText())
val originImagePath = "/data/${t.resources.i.src}"
val imageUrl="${resHtml.url}${originImagePath}"
val imageUrl = "${urlParam}${originImagePath}"
val imgRes: HttpResponse = client.get(imageUrl)
if (imgRes.status == HttpStatusCode.OK && imgRes.contentType() == ContentType.Image.JPEG) {
val filename="${it.index}.jpg"
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}")
urlResultChannel.send(UrlResult(originImagePath = originImagePath, t = t,
serverImagePath = serverImagePath))
webSocketChannel.send(
UrlResult(
originImagePath = originImagePath,
t = t,
serverImagePath = serverImagePath,
romajiTitle = romajiTitle,
filename = filename,
isLast = count() == it.index + 1
)
)
} else {
log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}")
}
} else {
log.warn("json url:${urlPath} 响应码:${jsonRes.status}")
}
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()))
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()
)
)
}
}
} else {
log.warn("http code:${response.status}")
webSocketChannel.send(StringResult(message = "${urlParam}解析失败"))
}
client.close()
}.apply {
currentJob = this
invokeOnCompletion {
log.info("${resHtml.url}解析完成")
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() {
@ -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
webSocket(Api.websocketPath) { // websocketSession
launch {
while (true) {
val urlResult = urlResultChannel.receive()
outgoing.send(Frame.Text(Json.encodeToString(urlResult)))
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
)
)
)
)
}
}
launch {
while (true) {
val task = taskChannel.receive()
outgoing.send(Frame.Text(Json.encodeToString(task)))
}
}
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val text = frame.readText()
when {
"cancel" == 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("当前服务器解析任务已停止"))
outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(
dataType = WebSocketResType.Cancel.name,
body = true
)
)
)
)
} else {
outgoing.send(Frame.Text("当前服务器没有正在运行的解析任务"))
outgoing.send(createFrameText(message = "当前服务器没有正在运行的解析任务"))
}
}
"exit" == text -> close(CloseReason(CloseReason.Codes.NORMAL, "Client said exit"))
else -> outgoing.send(createFrameText(message = "未知命令:${webSocketMessage.command}"))
}
} 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<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 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}:非法漫画地址"))
}

@ -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() {

@ -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() {

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

@ -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 {
@ -66,8 +73,10 @@ class ApplicationTest {
@Test
fun testUrl() {
println(Regex("/manga/\\w+")
.find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value)
println(
Regex("/manga/\\w+")
.find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value
)
}
@Test
@ -75,4 +84,48 @@ class ApplicationTest {
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<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