import io.ktor.application.* import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.html.* import io.ktor.http.* import io.ktor.http.cio.websocket.* 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.channels.Channel import kotlinx.coroutines.launch import kotlinx.html.HTML 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" //漫画url val urlChannel = Channel() //当前漫画解析任务 var currentJob: Job? = null //合成图片打包数据 val imageDataChannel = Channel() val webSocketChannel = Channel() //图片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("图片存储目录初始化失败") 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) .apply { 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("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}") } } 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() ) ) } } } 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 { val uploadDir = environment.config.property("ktor.deployment.filePath").getString() static(uploadDir) { resources(uploadDir) } val zipDir = environment.config.property("ktor.deployment.zipPath").getString() static(zipDir) { resources(zipDir) } static("/static") { resources() } webSocket(Api.websocketPath) { // websocketSession launch { while (true) { 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 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}")) } } 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.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)) { launch { urlChannel.send(urlParam.let { if (it.endsWith("/")) it else "$it/" }) } call.respond(MessageResponse(message = "初始化解析任务。。。")) } else { call.respond(MessageResponse(message = "${urlParam}:非法漫画地址")) } } } }